summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle32
-rw-r--r--app/lint.xml3
-rw-r--r--app/src/androidTest/java/se/leap/bitmaskclient/test/ConnectionManager.java33
-rw-r--r--app/src/androidTest/java/se/leap/bitmaskclient/test/testConfigurationWizard.java65
-rw-r--r--app/src/androidTest/java/se/leap/bitmaskclient/test/testDashboard.java94
-rw-r--r--app/src/androidTest/java/se/leap/bitmaskclient/test/testLeapSRPSession.java636
-rw-r--r--app/src/main/AndroidManifest.xml89
-rw-r--r--app/src/main/assets/minivpn.armeabibin0 -> 5216 bytes
-rw-r--r--app/src/main/assets/minivpn.armeabi-v7abin0 -> 5228 bytes
-rw-r--r--app/src/main/assets/minivpn.mipsbin0 -> 5164 bytes
-rw-r--r--app/src/main/assets/minivpn.x86bin0 -> 5268 bytes
-rw-r--r--app/src/main/assets/urls/bitmask demo.url3
-rw-r--r--app/src/main/java/org/jboss/security/srp/SRPParameters.java150
-rw-r--r--app/src/main/java/org/spongycastle/util/encoders/Base64.java121
-rw-r--r--app/src/main/java/org/spongycastle/util/encoders/Base64Encoder.java298
-rw-r--r--app/src/main/java/org/spongycastle/util/encoders/Encoder.java17
-rw-r--r--app/src/main/java/org/spongycastle/util/io/pem/PemGenerationException.java26
-rw-r--r--app/src/main/java/org/spongycastle/util/io/pem/PemHeader.java66
-rw-r--r--app/src/main/java/org/spongycastle/util/io/pem/PemObject.java62
-rw-r--r--app/src/main/java/org/spongycastle/util/io/pem/PemObjectGenerator.java7
-rw-r--r--app/src/main/java/org/spongycastle/util/io/pem/PemWriter.java138
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/AboutActivity.java40
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java194
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java581
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Dashboard.java479
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java94
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EIP.java623
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java306
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java77
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java341
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/LogInDialog.java151
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java123
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java338
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java212
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java875
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java56
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java115
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java114
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderListContent.java112
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java234
-rw-r--r--app/src/main/java/se/leap/openvpn/CIDRIP.java58
-rw-r--r--app/src/main/java/se/leap/openvpn/ConfigParser.java569
-rw-r--r--app/src/main/java/se/leap/openvpn/LICENSE.txt24
-rw-r--r--app/src/main/java/se/leap/openvpn/LaunchVPN.java385
-rw-r--r--app/src/main/java/se/leap/openvpn/LogWindow.java340
-rw-r--r--app/src/main/java/se/leap/openvpn/NetworkSateReceiver.java86
-rw-r--r--app/src/main/java/se/leap/openvpn/OpenVPN.java250
-rw-r--r--app/src/main/java/se/leap/openvpn/OpenVPNThread.java130
-rw-r--r--app/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java592
-rw-r--r--app/src/main/java/se/leap/openvpn/OpenVpnService.java504
-rw-r--r--app/src/main/java/se/leap/openvpn/ProfileManager.java220
-rw-r--r--app/src/main/java/se/leap/openvpn/ProxyDetection.java54
-rw-r--r--app/src/main/java/se/leap/openvpn/VPNLaunchHelper.java76
-rw-r--r--app/src/main/java/se/leap/openvpn/VpnProfile.java758
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_add.pngbin0 -> 2194 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_login.pngbin0 -> 1656 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.pngbin0 -> 1227 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.pngbin0 -> 1001 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.pngbin0 -> 773 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/ic_vpn_disconnected.pngbin0 -> 801 bytes
-rw-r--r--app/src/main/res/drawable-hdpi/icon.pngbin0 -> 7965 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_menu_add.pngbin0 -> 1580 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_menu_login.pngbin0 -> 1512 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_stat_vpn.pngbin0 -> 461 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/ic_vpn_disconnected.pngbin0 -> 455 bytes
-rw-r--r--app/src/main/res/drawable-ldpi/icon.pngbin0 -> 3118 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_add.pngbin0 -> 1339 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.pngbin0 -> 866 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.pngbin0 -> 746 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.pngbin0 -> 653 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/ic_vpn_disconnected.pngbin0 -> 586 bytes
-rw-r--r--app/src/main/res/drawable-mdpi/icon.pngbin0 -> 4518 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_login.pngbin0 -> 2178 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.pngbin0 -> 1622 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.pngbin0 -> 1279 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.pngbin0 -> 956 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.pngbin0 -> 1091 bytes
-rw-r--r--app/src/main/res/drawable-xhdpi/icon.pngbin0 -> 11135 bytes
-rw-r--r--app/src/main/res/layout-xlarge/about.xml134
-rw-r--r--app/src/main/res/layout-xlarge/client_dashboard.xml69
-rw-r--r--app/src/main/res/layout-xlarge/configuration_wizard_activity.xml27
-rw-r--r--app/src/main/res/layout-xlarge/eip_service_fragment.xml75
-rw-r--r--app/src/main/res/layout-xlarge/log_in_dialog.xml39
-rw-r--r--app/src/main/res/layout-xlarge/logwindow.xml17
-rw-r--r--app/src/main/res/layout-xlarge/new_provider_dialog.xml26
-rw-r--r--app/src/main/res/layout-xlarge/provider_detail_fragment.xml43
-rw-r--r--app/src/main/res/layout-xlarge/provider_list_fragment.xml16
-rw-r--r--app/src/main/res/layout-xlarge/provider_list_item.xml44
-rw-r--r--app/src/main/res/layout/about.xml122
-rw-r--r--app/src/main/res/layout/client_dashboard.xml67
-rw-r--r--app/src/main/res/layout/configuration_wizard_activity.xml26
-rw-r--r--app/src/main/res/layout/eip_service_fragment.xml71
-rw-r--r--app/src/main/res/layout/log_in_dialog.xml41
-rw-r--r--app/src/main/res/layout/logwindow.xml17
-rw-r--r--app/src/main/res/layout/new_provider_dialog.xml24
-rw-r--r--app/src/main/res/layout/provider_detail_fragment.xml40
-rw-r--r--app/src/main/res/layout/provider_list_fragment.xml15
-rw-r--r--app/src/main/res/layout/provider_list_item.xml43
-rw-r--r--app/src/main/res/menu/client_dashboard.xml23
-rw-r--r--app/src/main/res/menu/configuration_wizard_activity.xml15
-rw-r--r--app/src/main/res/menu/logmenu.xml34
-rw-r--r--app/src/main/res/values-ca/arrays.xml3
-rw-r--r--app/src/main/res/values-ca/strings.xml41
-rw-r--r--app/src/main/res/values-cs/arrays.xml3
-rw-r--r--app/src/main/res/values-cs/strings.xml79
-rw-r--r--app/src/main/res/values-de/arrays.xml3
-rw-r--r--app/src/main/res/values-de/strings.xml80
-rw-r--r--app/src/main/res/values-es/arrays.xml3
-rw-r--r--app/src/main/res/values-es/strings.xml60
-rw-r--r--app/src/main/res/values-et/arrays.xml3
-rw-r--r--app/src/main/res/values-et/strings.xml66
-rw-r--r--app/src/main/res/values-fr/arrays.xml3
-rw-r--r--app/src/main/res/values-fr/strings.xml65
-rw-r--r--app/src/main/res/values-id/arrays.xml3
-rw-r--r--app/src/main/res/values-id/strings.xml80
-rw-r--r--app/src/main/res/values-it/arrays.xml3
-rw-r--r--app/src/main/res/values-it/strings.xml66
-rw-r--r--app/src/main/res/values-ja/arrays.xml3
-rw-r--r--app/src/main/res/values-ja/strings.xml82
-rw-r--r--app/src/main/res/values-ko/arrays.xml3
-rw-r--r--app/src/main/res/values-ko/strings.xml65
-rw-r--r--app/src/main/res/values-nl/arrays.xml3
-rw-r--r--app/src/main/res/values-nl/strings.xml33
-rw-r--r--app/src/main/res/values-no/arrays.xml3
-rw-r--r--app/src/main/res/values-no/strings.xml38
-rw-r--r--app/src/main/res/values-ro/arrays.xml3
-rw-r--r--app/src/main/res/values-ro/strings.xml51
-rw-r--r--app/src/main/res/values-ru/arrays.xml3
-rw-r--r--app/src/main/res/values-ru/strings.xml80
-rw-r--r--app/src/main/res/values-uk/arrays.xml3
-rw-r--r--app/src/main/res/values-uk/strings.xml81
-rw-r--r--app/src/main/res/values-zh-rCN/arrays.xml3
-rw-r--r--app/src/main/res/values-zh-rCN/strings.xml52
-rw-r--r--app/src/main/res/values-zh-rTW/arrays.xml3
-rw-r--r--app/src/main/res/values-zh-rTW/strings.xml52
-rw-r--r--app/src/main/res/values/arrays.xml4
-rw-r--r--app/src/main/res/values/attrs.xml8
-rw-r--r--app/src/main/res/values/strings.xml143
-rw-r--r--app/src/main/res/values/styles.xml37
-rw-r--r--app/src/main/res/values/untranslatable.xml19
140 files changed, 13312 insertions, 0 deletions
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..68f47493
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,32 @@
+apply plugin: 'android'
+
+android {
+ compileSdkVersion 19
+ buildToolsVersion "19.0.1"
+
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 19
+
+ testPackageName "se.leap.bitmaskclient.test"
+ testInstrumentationRunner "android.test.InstrumentationTestRunner"
+ }
+
+ buildTypes {
+ release {
+ runProguard false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ sourceSets.main.jni.srcDirs = [] //disable automatic ndk-build call
+}
+
+dependencies {
+ androidTestCompile 'com.android.support:support-v4:+'
+ androidTestCompile 'com.jayway.android.robotium:robotium-solo:4.3.1'
+}
diff --git a/app/lint.xml b/app/lint.xml
new file mode 100644
index 00000000..ee0eead5
--- /dev/null
+++ b/app/lint.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+</lint> \ No newline at end of file
diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/ConnectionManager.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/ConnectionManager.java
new file mode 100644
index 00000000..f1cbff19
--- /dev/null
+++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/ConnectionManager.java
@@ -0,0 +1,33 @@
+package se.leap.bitmaskclient.test;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class ConnectionManager {
+ static void setMobileDataEnabled(boolean enabled, Context context) {
+ final ConnectivityManager conman = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ Method[] methods = conman.getClass().getMethods();
+ for (Method method : methods) {
+ if (method.getName().equals("setMobileDataEnabled")) {
+ method.setAccessible(true);
+ try {
+ method.invoke(conman, enabled);
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ static void setWifiEnabled(boolean enabled, Context context) {
+ WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ wifiManager.setWifiEnabled(enabled);
+ }
+}
diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/testConfigurationWizard.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/testConfigurationWizard.java
new file mode 100644
index 00000000..847d8cdd
--- /dev/null
+++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/testConfigurationWizard.java
@@ -0,0 +1,65 @@
+package se.leap.bitmaskclient.test;
+
+import android.test.ActivityInstrumentationTestCase2;
+import android.widget.ListView;
+import com.jayway.android.robotium.solo.Solo;
+import java.io.IOException;
+import se.leap.bitmaskclient.AboutActivity;
+import se.leap.bitmaskclient.ConfigurationWizard;
+import se.leap.bitmaskclient.ProviderDetailFragment;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.test.ConnectionManager;
+
+public class testConfigurationWizard extends ActivityInstrumentationTestCase2<ConfigurationWizard> {
+
+ private Solo solo;
+ private static int added_providers;
+
+ public testConfigurationWizard() {
+ super(ConfigurationWizard.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ solo = new Solo(getInstrumentation(), getActivity());
+ ConnectionManager.setMobileDataEnabled(true, solo.getCurrentActivity().getApplicationContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ solo.finishOpenedActivities();
+ }
+
+ public void testListProviders() throws IOException {
+ assertEquals(solo.getCurrentViews(ListView.class).size(), 1);
+
+ int number_of_available_providers = solo.getCurrentViews(ListView.class).get(0).getCount();
+
+ assertEquals("Number of available providers differ", solo.getCurrentActivity().getAssets().list("urls").length + added_providers, number_of_available_providers);
+ }
+
+ public void testSelectProvider() {
+ solo.clickOnText("bitmask");
+ assertTrue("Provider details dialog did not appear", solo.waitForFragmentByTag(ProviderDetailFragment.TAG, 60*1000));
+ }
+
+ public void testAddNewProvider() {
+ solo.clickOnActionBarItem(R.id.new_provider);
+ solo.enterText(0, "dev.bitmask.net");
+ solo.clickOnCheckBox(0);
+ solo.clickOnText(solo.getString(R.string.save));
+ added_providers = added_providers+1;
+ assertTrue("Provider details dialog did not appear", solo.waitForFragmentByTag(ProviderDetailFragment.TAG, 60*1000));
+ solo.goBack();
+ }
+
+ public void testShowAbout() {
+ solo.clickOnMenuItem(solo.getString(R.string.about));
+ assertTrue("Provider details dialog did not appear", solo.waitForActivity(AboutActivity.class));
+ }
+
+ public void testShowSettings() {
+ //TODO We still don't have the settings button
+ }
+}
diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/testDashboard.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/testDashboard.java
new file mode 100644
index 00000000..269f1d18
--- /dev/null
+++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/testDashboard.java
@@ -0,0 +1,94 @@
+package se.leap.bitmaskclient.test;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.provider.Settings;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+import com.jayway.android.robotium.solo.Solo;
+import se.leap.bitmaskclient.ConfigurationWizard;
+import se.leap.bitmaskclient.Dashboard;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.test.ConnectionManager;
+
+public class testDashboard extends ActivityInstrumentationTestCase2<Dashboard> {
+
+ private Solo solo;
+
+ public testDashboard() {
+ super(Dashboard.class);
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ solo = new Solo(getInstrumentation(), getActivity());
+ ConnectionManager.setMobileDataEnabled(true, solo.getCurrentActivity().getApplicationContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ solo.finishOpenedActivities();
+ }
+
+ /**
+ * This test will fail if Android does not trust VPN connection.
+ * I cannot automate that dialog.
+ */
+ public void testOnOffOpenVpn() {
+ solo.clickOnView(solo.getView(R.id.eipSwitch));
+ if(!solo.waitForText(getActivity().getString(R.string.eip_status_start_pending)))
+ fail();
+ if(!solo.waitForText(getActivity().getString(R.string.state_auth)))
+ fail();
+ if(!solo.waitForText(getActivity().getString(R.string.eip_state_connected), 1, 30*1000))
+ fail();
+
+ solo.clickOnView(solo.getView(R.id.eipSwitch));
+ if(!solo.waitForText(getActivity().getString(R.string.eip_state_not_connected)))
+ fail();
+
+ ConnectionManager.setMobileDataEnabled(false, solo.getCurrentActivity().getApplicationContext());
+
+ solo.clickOnView(solo.getView(R.id.eipSwitch));
+ if(!solo.waitForText(getActivity().getString(R.string.eip_status_start_pending)))
+ fail();
+ if(!solo.waitForText(getActivity().getString(R.string.state_nonetwork)))
+ fail();
+
+ }
+
+ public void testLogInAndOut() {
+ long miliseconds_to_log_in = 40 * 1000;
+ solo.clickOnActionBarItem(R.id.login_button);
+ solo.enterText(0, "parmegvtest1");
+ solo.enterText(1, " S_Zw3'-");
+ solo.clickOnText("Log In");
+ solo.waitForDialogToClose();
+ solo.waitForDialogToClose(miliseconds_to_log_in);
+ if(!solo.waitForText(getActivity().getString(R.string.succesful_authentication_message)))
+ fail();
+
+ solo.clickOnActionBarItem(R.string.logout_button);
+ if(!solo.waitForDialogToClose())
+ fail();
+ }
+
+ public void testShowAbout() {
+ solo.clickOnMenuItem(getActivity().getString(R.string.about));
+ solo.waitForText(getActivity().getString(R.string.repository_url_text));
+ solo.goBack();
+
+ solo.clickOnMenuItem(getActivity().getString(R.string.about));
+ solo.waitForText(getActivity().getString(R.string.repository_url_text));
+ solo.goBack();
+ }
+
+ public void testSwitchProvider() {
+ solo.clickOnMenuItem(getActivity().getString(R.string.switch_provider_menu_option));
+ solo.waitForActivity(ConfigurationWizard.class);
+ solo.goBack();
+ }
+}
diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/testLeapSRPSession.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/testLeapSRPSession.java
new file mode 100644
index 00000000..88d70da4
--- /dev/null
+++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/testLeapSRPSession.java
@@ -0,0 +1,636 @@
+package se.leap.bitmaskclient.test;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import org.jboss.security.srp.SRPParameters;
+
+import android.test.suitebuilder.annotation.SmallTest;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.LeapSRPSession;
+
+import junit.framework.TestCase;
+
+public class testLeapSRPSession extends TestCase {
+
+ public testLeapSRPSession(String name) {
+ super(name);
+ }
+
+ protected void setUp() throws Exception {
+ super.setUp();
+ }
+
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void testExponential() {
+ byte[] expected_A;
+ byte[] a_byte;
+ SRPParameters params;
+ LeapSRPSession client;
+
+ /* Test 1: abytes = 4 */
+ expected_A = new BigInteger("44eba0239ddfcc5a488d208df32a89eb00e93e6576b22ba2e4410085a413cf64e9c2f08ebc36a788a0761391150ad4a0507ca43f9ca659e2734f0457a85358c0bb39fa87183c9d3f9f8a3b148dab6303a4e796294f3e956472ba0e2ea5697382acd93c8b8f1b3a7a9d8517eebffd6301bfc8de7f7b701f0878a71faae1e25ad4", 16).toByteArray();
+ String username = "username",
+ password = "password",
+ salt = "64c3289d04a6ecad",
+ a = "3565fdc2";
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ byte[] A = client.exponential();
+
+ assertTrue(Arrays.equals(A, expected_A));
+
+ /* Test 1: abytes = 5 */
+ a = "67c152a3";
+ expected_A = new BigInteger("11acfacc08178d48f95c0e69adb11f6d144dd0980ee6e44b391347592e3bd5e9cb841d243b3d9ac2adb25b367a2558e8829b22dcef96c0934378412383ccf95141c3cb5f17ada20f53a0225f56a07f2b0c0469ed6bbad3646f7b71bdd4bedf5cc6fac244b26d3195d8f55877ff94a925b0c0c8f7273eca733c0355b38360442e", 16).toByteArray();
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ A = client.exponential();
+
+ assertTrue(Arrays.equals(A, expected_A));
+ }
+
+ public void testResponse() throws NoSuchAlgorithmException {
+ /* Test 1: with intermediate checks */
+ byte[] expected_A = trim(new BigInteger("884380f70a62193bbe3589c4e1dbdc4467b6b5a1b4486e4b779023506fc1f885ae26fa4a5d817b3f38a35f3487b147b82d4bd0069faa64fdc845f7494a78251709e212698e42ced44b0f3849adc73f467afcb26983bd13bdc38906b178003373ddd0ac1d38ce8a39ffa3a7795787207a129a784f4b65ce0b302eb1bcf4045883", 16).toByteArray());
+ byte[] expected_x = new BigInteger("4cb937fd74ee3bb53b79a3174d0c07c14131de9c825897cbca52154e74200602", 16).toByteArray();
+ byte[] expected_M1 = trim(new BigInteger("e6a8efca2c07ef24e0b69be2d4d4a7e74742a4db7a92228218fec0008f7cc94b", 16).toByteArray());
+ String username = "username",
+ password = "password",
+ salt = "64c3289d04a6ecad",
+ a = "8c911355";
+ byte[] a_byte = new BigInteger(a, 16).toByteArray();
+ SRPParameters params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ LeapSRPSession client = new LeapSRPSession(username, password, params, a_byte);
+
+ byte[] x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ assertTrue(Arrays.equals(x, expected_x));
+
+ byte[] A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ String B = "bc745ba25564fc312f44ea09fb663aa6d95867772e412a6a23f1bc24183e54b32f134372c560f4b3fda19ba7a56b0f84fdcdecc22be6fd256639e918e019691c40a39aa5c9631820e42b28da61b8c75b45afae9d77d63ac8f4dda093762be4a890fbd86061dbd7e5e7c03c4dacde769e0f564df00403e449c0535537f1ba7263";
+
+ byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ /* Test 2: no intermediate checks */
+ expected_A = trim(new BigInteger("9ffc407afd7e7ecd32a8ea68aa782b0254a7e2197a955b5aa646fc1fc43ff6ef2239f01b7d5b82f152c870d3e69f3321878ca2acda06dd8fb6ce02f41c7ed48061c78697b01cf353f4222311334c707358b6ec067e317527316bfa85b5ec74537e38b5b14c1100d14c96320f385e5b1dcccde07e728c7ef624353167a29ae461", 16).toByteArray());
+ expected_M1 = trim(new BigInteger("c3203ec1dd55c96038276456c18c447fb4d2a2f896c73c31d56da1781cae79a8", 16).toByteArray());
+ a = "38d5b211";
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+ x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ A = client.exponential();
+
+ B = "b8ca7d93dbe2478966ffe025a9f2fb43b9995ce04af9523ea9a3fa4b132136076aa66ead1597c3da23f477ce1cfaf68b5dcc94e146db06cf8391d14a76ce53aab86067b13c93b135d7be6275669b3f51afec6cc41f19e0afca7c3ad5c4d6ee4c09d4b11bcd12e26c727ee56d173b92eea6926e72cc73deebe12dd6f30f44db8a";
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ /* Test 3: With intermediate values */
+ expected_M1 = new BigInteger("4c01f65a9bb00f95e435593083040ae1e59e59800c598b42de821c21f3a35223", 16).toByteArray();
+ expected_A = new BigInteger("1bceab3047a6f84495fdd5b4dbe891f0b30f870d7d4e38eaef728f6a7d4e9342d8dae8502fdae4f16b718d2e916a38b16b6def45559a5ebae417a1b115ba6b6a0451c7ff174c3e2507d7d1f18ef646fd065bc9ba165a2a0ae4d6da54f060427178b95b0eff745f5c3f8c4f19ea35addc3ce0daf2aca3328c98bafcf98286d115", 16).toByteArray();
+ B = "41a7b384f2f52312fa79b9dc650ae894f543aea49800cf9477fbcf63e39cbfe6d422f7126777d645cdf749276a3ae9eb6dfcfdb887f8f60ac4094a343013fcebbd40e95b3153f403ab7bb21ea1315aa018bab6ab84017fcb084b3870a8bf1cb717b39c9a28177c61ce7d1738379be9d192dd9793b50ebc3afabe5e78b0a4b017";
+ a = "36ee80ec";
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+ x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ A = client.exponential();
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+ }
+
+ public void testCalculateV() throws NoSuchAlgorithmException {
+ String expected_v = "502f3ffddc78b866330550c2c60ebd68427c1793237d770e6390d1f794abd47b6786fa5025728d1ca4ec24cfc7a89808278330099ad66456a7c9c88be570b928f9825ac2ecdee31792335f7fa5fc9a78b692c487aa400c7d5cc5c1f2a3a74634c4afa0159600bbf22bf6dfb1e0d85061e55ce8df6243758066503bcf51c83848cf7184209731f89a90d888934c75798828859babe73c17009bf827723fc1bcd0";
+
+ BigInteger k = new BigInteger("bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0", 16);
+ BigInteger g = new BigInteger("2", 16);
+ BigInteger N = new BigInteger(ConfigHelper.NG_1024, 16);
+ BigInteger x = new BigInteger("4cb937fd74ee3bb53b79a3174d0c07c14131de9c825897cbca52154e74200602", 16);
+
+ BigInteger v = k.multiply(g.modPow(x, N)); // g^x % N
+
+ assertEquals(v.toString(16), expected_v);
+ assertTrue(Arrays.equals(v.toByteArray(), new BigInteger(expected_v, 16).toByteArray()));
+ }
+
+ public void testGetU() throws NoSuchAlgorithmException {
+ /* Test 1 */
+ String Astr = "46b1d1fe038a617966821bd5bb6af967be1bcd6f54c2db5a474cb80b625870e4616953271501a82198d0c14e72b95cdcfc9ec867027b0389aacb313319d4e81604ccf09ce7841dc333be2e03610ae46ec0c8e06b8e86975e0984cae4d0b61c51f1fe5499a4d4d42460261a3e134f841f2cef4d68a583130ee8d730e0b51a858f";
+ String Bstr = "5e1a9ac84b1d9212a0d8f8fe444a34e7da4556a1ef5aebc043ae7276099ccdb305fd7e1c179729e24b484a35c0e33b6a898477590b93e9a4044fc1b8d6bc73db8ac7778f546b25ec3f22e92ab7144e5b974dc58e82a333262063062b6944a2e4393d2a087e4906e6a8cfa0fdfd8a5e5930b7cdb45435cbee7c49dfa1d1216881";
+ String ustr = "759c3cfb6bfaccf07eeb8e46fe6ea290291d4d32faca0681830a372983ab0a61";
+
+ byte[] Abytes = new BigInteger(Astr, 16).toByteArray();
+ byte[] Bbytes = new BigInteger(Bstr, 16).toByteArray();
+ byte[] expected_u = new BigInteger(ustr, 16).toByteArray();
+
+ MessageDigest u_digest = MessageDigest.getInstance("SHA256");
+ u_digest.update(trim(Abytes));
+ u_digest.update(trim(Bbytes));
+ byte[] u = new BigInteger(1, u_digest.digest()).toByteArray();
+
+ assertTrue(Arrays.equals(expected_u, u));
+
+ /* Test 2 */
+ Astr = "c4013381bdb2fdd901944b9d823360f367c52635b576b9a50d2db77141d357ed391c3ac5fa452c2bbdc35f96bfed21df61627b40aed8f67f21ebf81e5621333f44049d6c9f6ad36464041438350e1f86000a8e3bfb63d4128c18322d2517b0d3ead63fd504a9c8f2156d46e64268110cec5f3ccab54a21559c7ab3ad67fedf90";
+ Bstr = "e5d988752e8f265f01b98a1dcdecc4b685bd512e7cd9507f3c29f206c27dac91e027641eed1765c4603bbd7a9aa7fac300ef67dafe611ba2dbe29a32d83d486296f328d38b44c0c211d01d3fe422aac168b6850c87782338969c54594fc87804d4db34910ad4b5452a81027842ac8d8d8288fd44872e4c719ac8fb971d0a33e1";
+ ustr = "6510328f913b81ba662e564ee5afb7c395ea27c3c0276fc5ca51f0edecd4baf1";
+
+ Abytes = new BigInteger(Astr, 16).toByteArray();
+ Bbytes = new BigInteger(Bstr, 16).toByteArray();
+ expected_u = new BigInteger(ustr, 16).toByteArray();
+ expected_u = trim(expected_u);
+
+ u_digest = MessageDigest.getInstance("SHA-256");
+ u_digest.update(trim(Abytes));
+ u_digest.update(trim(Bbytes));
+ u = new BigInteger(1, u_digest.digest()).toByteArray();
+ u = trim(u);
+
+ assertTrue(Arrays.equals(expected_u, u));
+
+ /* Test 3 */
+ Astr = "d13973fe4e0e13423cd036caf0912e23a1f9b0c23966f5a5897c8ff17c5cbac8bab7f07d9ac4ee47396a7c68e80ce854c84f243148521277900aaa132a7b93b61e54d742d7f36edb4cdef54bc78cca69ac72653a7ae0fc47ec1e9a84024ea9487a61357e28eddc185e4fe01388e64e6b8f688dd74471d56dd244204522e08483";
+ Bstr = "a6701686d9d987a43f06e8497330c8add8febd191a7a975bced0d058eb03ccc6805263349363b2d54ac435b01155dc41c6067287d9b93e3637ab3b7e8bc7d9cf38d9fdbb2ca9ee8ba1946a46cb555cb7dafcc177fcf7a4b0eb1e5db2249949c1fd15e0b7c1b3616f9e2649bdf074ed841efbdc9f29ee8c8bfcedeaed3dc49378";
+ ustr = "78414ec80cf44225e7ed386dcf2ceb89837327ccae11b761fc77d48c0307977";
+
+ Abytes = new BigInteger(Astr, 16).toByteArray();
+ Bbytes = new BigInteger(Bstr, 16).toByteArray();
+ expected_u = new BigInteger(ustr, 16).toByteArray();
+ expected_u = trim(expected_u);
+
+ u_digest = MessageDigest.getInstance("SHA-256");
+ u_digest.update(trim(Abytes));
+ u_digest.update(trim(Bbytes));
+ u = new BigInteger(1, u_digest.digest()).toByteArray();
+ u = trim(u);
+
+ assertTrue(Arrays.equals(expected_u, u));
+
+ /* Test 4 */
+ Astr = "ee8bc0cb97dd9c9937759658ff9d791df1dd57b48c5febc2e98af028d0e36eaddf1a3fc555f2bcd6456827e8c7b07ec02a1f365457843bda226bfc1a55c4776879f9df6c916810131ec65a3a4cf473c6a34299d64c91cf26542ea0fc059d24422fc783460c3fafe26bf6f7c24904ae1c5a6421e2f5315030ab007ce8f2c2fd97";
+ Bstr = "95ecbd13b28c7f38318fd664ee97d9e853b0d6e9cbff9a3775a3cc5d5077ffc146aec70d9439e75c19a34b67368b8bd7035ba6254e0a260d99b1e253aae2e0d8f4a13e1ed472f3ef0e3086300cd15d059f6be7d7141ee09071b1c5e5d1c83b250a3c8f1a587f8fe59d49aaeb2cfc7e13a5a58bc76cc8baf7f6a647982c67ee49";
+ ustr = "e28737c7307c84e4d0866b7cf882f22852a764b109634f77a5eb986a96ffcf9a";
+
+ Abytes = new BigInteger(Astr, 16).toByteArray();
+ Bbytes = new BigInteger(Bstr, 16).toByteArray();
+ expected_u = new BigInteger(ustr, 16).toByteArray();
+ expected_u = trim(expected_u);
+ assertEquals(new BigInteger(1, expected_u).toString(16), ustr);
+
+ u_digest = MessageDigest.getInstance("SHA-256");
+ u_digest.update(trim(Abytes));
+ u_digest.update(trim(Bbytes));
+ u = new BigInteger(1, u_digest.digest()).toByteArray();
+ u = trim(u);
+
+ assertTrue(Arrays.equals(expected_u, u));
+ }
+
+ @SmallTest
+ public void testCalculatePasswordHash() throws UnsupportedEncodingException, NoSuchAlgorithmException {
+ String salt_str = "", username_str = "", password_str = "";
+ String expected_inner = "cfb9ae3ec5433076889c4fe5663926e20bf570cc7950a51c889a314fab2f5ed716bde9c1cc91be14",
+ expected_x = "9736a5e386a18f35bb08cac0f7c70bdbe120f2efe019874d0eb23b85b1955858";
+
+ /* Test 1 */
+ salt_str = "cfb9ae3ec5433076"; username_str = "nostradamus"; password_str = "$[[//jjiilajfewahug43a89y¿";
+ password_str = password_str.replaceAll("\\\\", "\\\\\\\\");
+ // Calculate x = H(s | H(U | ':' | password))
+ MessageDigest x_digest = MessageDigest.getInstance("SHA-256");
+
+ // Try to convert the username to a byte[] using UTF-8
+ byte[] user = null;
+ byte[] password_bytes = null;
+ byte[] colon = {};
+
+ String encoding = "UTF-8";
+ encoding = "ISO-8859-1";
+ user = ConfigHelper.trim(username_str.getBytes(encoding));
+ colon = ConfigHelper.trim(":".getBytes(encoding));
+ password_bytes = ConfigHelper.trim(password_str.getBytes(encoding));
+
+ // Build the hash
+ x_digest.update(user);
+ x_digest.update(colon);
+ x_digest.update(password_bytes);
+ byte[] h = x_digest.digest();
+ byte[] salt_bytes = ConfigHelper.trim(new BigInteger(salt_str, 16).toByteArray());
+
+ x_digest.reset();
+ x_digest.update(salt_bytes);
+ x_digest.update(h);
+ byte[] x_digest_bytes = x_digest.digest();
+ assertTrue(new BigInteger(1, x_digest_bytes).toString(16).equalsIgnoreCase(expected_x));
+ }
+
+ public void testCalculateS() throws NoSuchAlgorithmException {
+ String expected_S = "34d71467d0a30c5787e6b4384c7176a724df28f6c1d3b0b7738238621be19080942ca5d4454ab3e5664d42970ea2d42d1188ab246afb1475c481111f30886c79cc99ddd1d711034bc4ac57a0c134fef40470e1ac3386f39abcd1bf9d0cc758a1f38a87b2f7261018cdc6191cd941292c77c320b3c664a9bb1325dd7a2bf62db4";
+
+ String B_string = "d390da0991b84e20918764aed9bccf36fc70863f08a1d3fa0aecd9610d8c40d1895afaf3aa9b4582fb5bc673037dfc1bf0ca7d3d190a807dec563fe57d596b3551ec8a6461e0621b77fabfaa29234187063e715e2706522c65f35d89f3914a7cdf40ddd240ef3d22b5534469cf6b3b31e158f817dcf5b6b44b0914d4787d027e";
+ String A_string = "c6385e56b9cb8987df3c58ffe6c47bfcd9f9d52f02b68048d72135a8522a0919b41683e3c732a0874e0555b8f288fa067842b1c40a56f1a7d910ad125e71238cb14ba47838f881cabf735d5414f4fcdd6855943621c271c5f013753d439fb124674c79e30d849e9178c780f7c25aded12f053ae41be68289787caf7c3cbb07a4";
+ String a_string = "fc1c2d8d";
+
+ BigInteger A = new BigInteger(A_string, 16);
+ byte[] Bbytes = new BigInteger(B_string, 16).toByteArray();
+ BigInteger a = new BigInteger(a_string, 16);
+ BigInteger g = ConfigHelper.G;
+ BigInteger N = new BigInteger(ConfigHelper.NG_1024, 16);
+ BigInteger k = new BigInteger("bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0", 16);
+ BigInteger x = new BigInteger("4cb937fd74ee3bb53b79a3174d0c07c14131de9c825897cbca52154e74200602", 16);
+
+ /* GetU */
+ MessageDigest u_digest = MessageDigest.getInstance("SHA256");
+ u_digest.update(trim(A.toByteArray()));
+ u_digest.update(trim(Bbytes));
+ byte[] ub = new BigInteger(1, u_digest.digest()).toByteArray();
+
+ BigInteger v = k.multiply(g.modPow(x, N)); // g^x % N
+ String expected_v = "502f3ffddc78b866330550c2c60ebd68427c1793237d770e6390d1f794abd47b6786fa5025728d1ca4ec24cfc7a89808278330099ad66456a7c9c88be570b928f9825ac2ecdee31792335f7fa5fc9a78b692c487aa400c7d5cc5c1f2a3a74634c4afa0159600bbf22bf6dfb1e0d85061e55ce8df6243758066503bcf51c83848cf7184209731f89a90d888934c75798828859babe73c17009bf827723fc1bcd0";
+ String v_string = v.toString(16);
+
+ BigInteger B = new BigInteger(1, Bbytes);
+ BigInteger u = new BigInteger(1, ub);
+ String expected_u_string = "2d36f816df24da7b904c904a7e2a2500511df118ced26bda92a63aca792c93b";
+ String u_string = u.toString(16);
+
+ BigInteger B_v = B.subtract(v);
+ BigInteger a_ux = a.add(u.multiply(x));
+ byte[] a_ux_byte= a_ux.toByteArray();
+ String expected_a_aux_string = "d8d0843a60d471ab46a761b775f8fd28bbbd8affbc704424c79b822ac36b1177925404db910072737df3c5083cf20ebd2f08e1381d80d91e4f6fc99f592203";
+ byte[] expected_a_aux_byte = new BigInteger(expected_a_aux_string, 16).toByteArray();
+
+ BigInteger S = B_v.modPow(a_ux, N);
+
+ byte[] expected_S_bytes = new BigInteger(expected_S, 16).toByteArray();
+ byte[] S_bytes = S.toByteArray();
+ assertTrue(Arrays.equals(S_bytes, expected_S_bytes));
+ assertEquals(S.toString(16), expected_S);
+ }
+
+ public void testXor() throws Exception {
+ String expected_xor_string = "928ade491bc87bba9eb578701d44d30ed9080e60e542ba0d3b9c20ded9f592bf";
+ byte[] expected_xor = new BigInteger(expected_xor_string, 16).toByteArray();
+
+ byte[] nbytes = trim(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray());
+ byte[] gbytes = trim(ConfigHelper.G.toByteArray());
+
+ byte[] ndigest = trim(MessageDigest.getInstance("SHA-256").digest(nbytes));
+ byte[] gdigest = MessageDigest.getInstance("SHA-256").digest(gbytes);
+
+ BigInteger ndigest_bigInteger = new BigInteger(1, ndigest);
+ String expected_ndigest_string = "494b6a801b379f37c9ee25d5db7cd70ffcfe53d01b7c9e4470eaca46bda24b39";
+ String ndigest_string = ndigest_bigInteger.toString(16);
+ assertEquals(ndigest_string, expected_ndigest_string);
+
+ BigInteger gdigest_bigInteger = new BigInteger(1, gdigest);
+ String xor_string = ndigest_bigInteger.xor(gdigest_bigInteger).toString(16);
+
+ byte[] xor = new BigInteger(xor_string, 16).toByteArray();
+
+ assertTrue(Arrays.equals(expected_xor, xor));
+ }
+
+ public void testVerify() throws NoSuchAlgorithmException {
+ byte[] expected_A = trim(new BigInteger("884380f70a62193bbe3589c4e1dbdc4467b6b5a1b4486e4b779023506fc1f885ae26fa4a5d817b3f38a35f3487b147b82d4bd0069faa64fdc845f7494a78251709e212698e42ced44b0f3849adc73f467afcb26983bd13bdc38906b178003373ddd0ac1d38ce8a39ffa3a7795787207a129a784f4b65ce0b302eb1bcf4045883", 16).toByteArray());
+ byte[] expected_x = new BigInteger("4cb937fd74ee3bb53b79a3174d0c07c14131de9c825897cbca52154e74200602", 16).toByteArray();
+ byte[] expected_M1 = trim(new BigInteger("e6a8efca2c07ef24e0b69be2d4d4a7e74742a4db7a92228218fec0008f7cc94b", 16).toByteArray());
+ byte[] expected_M2 = trim(new BigInteger("6402e108415ab4a7cd223ec435570614c8aacc09fcf081ade2dc00275e90ceee", 16).toByteArray());
+ String username = "username",
+ password = "password",
+ salt = "64c3289d04a6ecad",
+ a = "8c911355";
+ byte[] a_byte = new BigInteger(a, 16).toByteArray();
+ SRPParameters params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ LeapSRPSession client = new LeapSRPSession(username, password, params, a_byte);
+
+ byte[] x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ assertTrue(Arrays.equals(x, expected_x));
+
+ byte[] A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ String B = "bc745ba25564fc312f44ea09fb663aa6d95867772e412a6a23f1bc24183e54b32f134372c560f4b3fda19ba7a56b0f84fdcdecc22be6fd256639e918e019691c40a39aa5c9631820e42b28da61b8c75b45afae9d77d63ac8f4dda093762be4a890fbd86061dbd7e5e7c03c4dacde769e0f564df00403e449c0535537f1ba7263";
+
+ byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ boolean verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 2 */
+ expected_A = trim(new BigInteger("180a1caf84efe93610a56772edea7b2d20ef3e9f34e578147b5402a898982f33131708233f9ddd2946246703c5db705f0859cca9cfc5b72ad5a05ec0c748545aa083d5b7b1bf06efe6737e9e0fd81b832b5cba983f1b9717041df8114385b93c8c669db06d62c5773b8e8a8f07e98a840a33d04d3448d4bcd2c042387c316750", 16).toByteArray());
+ expected_M1 = trim(new BigInteger("a47782f23057a7e06704ea94389589b3c70971a63268acef2aefd74e234dd3c2", 16).toByteArray());
+ a = "d89f0e33";
+ expected_M2 = trim(new BigInteger("517278a03a0320a52dcb391caf5264d76149d7d9b71ed2b65536233344c550cf", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+ x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ A = client.exponential();
+
+ B = "5f86fe2f7b7455e877e1760db8d3da1fcd4df0d10ec2a40298f87287bdb2f22c0ea54ff9b1f660cc1666459a7e2fd5501970b317490c3dfd3ba2e18f7be7526b72ea4d01e8f064754b935b107ced0892ce86112cbe32282f929907985fcb29f42c5d4dc32adeb29d12a611cac49cca3fefd2227efadc3989c2e72dd64a003141";
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 3 */
+ expected_A = trim(new BigInteger("a9c556c30bf4c1b1fdc1bc9e672ab4751806acc8581042b3779faaf25f85f47dfc58828742e2d2a06c51acbbb9f3fae0e01f64df0775a269f5ee4a6e71bc37b8a368e04b9053d399bc5b809ffd6ecab775a577804f2a5ed2e829f15e6af13bf0b78b6b108cf591bc9960992904fd1433698a51e0d05ee954cf98cbfe7995621e", 16).toByteArray());
+ expected_M1 = trim(new BigInteger("0afca3583c4146990ec7312f9f4b4d9cceebc43a19f96709bf3d0a17b11dcc1e", 16).toByteArray());
+ a = "50e662d6";
+ expected_M2 = trim(new BigInteger("3bfb91c7d04b6da6381fe3d2648d992cdc6bc67b8ee16d1cfa733f786d492261", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+ x = client.calculatePasswordHash(username, password, new BigInteger(salt, 16).toByteArray());
+ A = client.exponential();
+
+ B = "6fe41e8262f4f8bc4ed9f4e1b4802ae3adac9c348e6efc07f16c6f5704b95a1f12325097489372c3936584a37301ebab400a32ac6699f4556da84f076489060527bd50578a317a3ec8b814bf2f4dd9c4adad368610eb638aa81663a205ba26d8f0b9654bf3940357b867cd42725e8532b97a2410a557d291aa55c0b44f249361";
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 4: user abc, password abcdefghi */
+ username = "abc";
+ password = "abcdefghi";
+ salt = "be26aac449a093e5";
+ expected_A = trim(new BigInteger("c4013381bdb2fdd901944b9d823360f367c52635b576b9a50d2db77141d357ed391c3ac5fa452c2bbdc35f96bfed21df61627b40aed8f67f21ebf81e5621333f44049d6c9f6ad36464041438350e1f86000a8e3bfb63d4128c18322d2517b0d3ead63fd504a9c8f2156d46e64268110cec5f3ccab54a21559c7ab3ad67fedf90", 16).toByteArray());
+ expected_x = trim(new BigInteger("6325967f1a161efd4e2d6e6fabbfccc32be05139cf82b08fb59c0a0db3f34bcf", 16).toByteArray());
+ a = "5d4cde29";
+ B = "e5d988752e8f265f01b98a1dcdecc4b685bd512e7cd9507f3c29f206c27dac91e027641eed1765c4603bbd7a9aa7fac300ef67dafe611ba2dbe29a32d83d486296f328d38b44c0c211d01d3fe422aac168b6850c87782338969c54594fc87804d4db34910ad4b5452a81027842ac8d8d8288fd44872e4c719ac8fb971d0a33e1";
+ expected_M1 = trim(new BigInteger("e5972ddc53e6190735fc79cd823053a65ffb6041d69480adcba2f6a2dc2f2e86", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("8f4552b1021a4de621d8f50f0921c4d20651e702d9d71276f8f6c15b838de018", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(trim(new BigInteger(salt, 16).toByteArray()), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 5: user abc, password abcdefghi */
+ username = "abc";
+ password = "abcdefghi";
+ salt = "be26aac449a093e5";
+ expected_A = trim(new BigInteger("d13973fe4e0e13423cd036caf0912e23a1f9b0c23966f5a5897c8ff17c5cbac8bab7f07d9ac4ee47396a7c68e80ce854c84f243148521277900aaa132a7b93b61e54d742d7f36edb4cdef54bc78cca69ac72653a7ae0fc47ec1e9a84024ea9487a61357e28eddc185e4fe01388e64e6b8f688dd74471d56dd244204522e08483", 16).toByteArray());
+ expected_x = trim(new BigInteger("6325967f1a161efd4e2d6e6fabbfccc32be05139cf82b08fb59c0a0db3f34bcf", 16).toByteArray());
+ a = "fc57e4b1";
+ B = "a6701686d9d987a43f06e8497330c8add8febd191a7a975bced0d058eb03ccc6805263349363b2d54ac435b01155dc41c6067287d9b93e3637ab3b7e8bc7d9cf38d9fdbb2ca9ee8ba1946a46cb555cb7dafcc177fcf7a4b0eb1e5db2249949c1fd15e0b7c1b3616f9e2649bdf074ed841efbdc9f29ee8c8bfcedeaed3dc49378";
+ expected_M1 = trim(new BigInteger("0b590fde631566d0d3420a898a9b469656e64bfaff165c146b78964eee7920b8", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("04cf3ab3b75dbc4b116ca2fec949bf3deca1e360e016d7ab2b8a49904c534a27", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(trim(new BigInteger(salt, 16).toByteArray()), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 6: user and password of real use */
+ username = "parmegv";
+ password = "wR\"P}x@_,:k$`Y<i7PH9\\\\zubHtn[-4MoL+$(?k>Yd*s T`-n.";
+ salt = "40c3f47b99ce8dc9";
+ expected_A = trim(new BigInteger("490b5de7a287c59cefe267441a186ec24f63210fbf28877305f5896eaec5a7245d304ecb2b09d91066e627d7b2c8bf9e5271d882361a435355d1c2d1ac9d3069877189a01d64b2dd73a569e9e96b9a99767dbc02e04c839b09444f48430b113c1827c20b684ae33f5018051169f5acf4913ebd76a205c6f1aa2cc75747687d56", 16).toByteArray());
+ String x_string = "9665839759b4fb9684e7438daecbd6e7129b4ebd3e4a107916e9a64bbbf399c9";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ a = "a72111a2";
+ B = "6574ddce3e33c44a77198fa8b3656627e4a24c8786948e79f0c2588febaa485c94b1deb5e420bd3b46f9a34c7862525452ca7a0542c52f939d9f277a013aeceef7353a7741440f6dd2f6c2f1dc07fa5ca003e305c89c876a3035bd04f546b711d44da06a3ba827bc8affbf9ed46de1bfbc670ef9ed7c0bb8cdc588285d13849e";
+ expected_M1 = trim(new BigInteger("03bbcf57aeaec89a3a254bb9650a924ea86aa0fdd83fd7274a75b7083f221cf0", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("082cf49ad5a34cc5ca571e3d063aec4bd96e7b96a6d951295180631650a84587", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 7: password with ! */
+ username = "parmegvtest3";
+ password = "holahola!";
+ salt = "1bf48d42b9a7ed32";
+ expected_A = trim(new BigInteger("4e2fbe8db5d07f33ff1f4303959b5396dcffc1460b6ce8866fd388415f27fe10f9042986ab8682cdcf9a033e651bca89173688989adad854c91bc1918f98d5c82525fb6f328a8cf74ce1436b23821cba5337aaa20a3e5631e4b957053d542f2b5fc456e888371c9d6b94360b37adb2793eca8db100c24887c459e36d729a98e1", 16).toByteArray());
+ x_string = "363d1d62dda07b2d987a9739ddb5ec32fcad9c7322fb64e87937f2da86c45d9f";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ a = "16dd0cf5";
+ B = "dd5c9c5e13eb5daa6e7303928b3c826cec520ccef429c0dcb785be34c330d5bb89c99d7d94842b6b5c19cac600f884c50b26989b105f397115df7f3d13c5c7c2f6327cc547fc854ae40f09f1f6a104968bd510243feb104eb559e085fe1d720770be2887a1f424c534a3ab962d82e92458f652328bcf9878f95fdcf463d06193";
+ expected_M1 = trim(new BigInteger("a7ffbff753a547b877f8944339b707b3ce1998da27badf253d56bf39f35308a6", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("5cc3d7f0077e978c83acdef14a725af01488c1728f0cf32cd7013d24faf5d901", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 8: username and password *was* failing in localhost testProviderAPI*/
+ username = "gg";
+ password = "password";
+ a = "bc925bfb";
+ salt = "ff9ebb44e947cf59";
+ expected_A = trim(new BigInteger("8f434633414faeaf035a0dea8c1cb7876bb1f8ee80d6fee8ea43ae60c4f9658550d825c25f1ed5c6a5543358bbcb559b76958c8047a2e7e5fe0072bc1f16401bcfa77b57651ff50dd665c6f28c302b37c98495eff397a56befead2e5ceffaace45f2ec200520258adb66df751e815e464656d869454e360d98cbc70f9c64fd4c", 16).toByteArray());
+ x_string = "9cad2eca264380dd0b48e3b405e109c1be0615ee6ec92e7105eff5bc3a309fd9";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ B = "9ca2cd50b4c41047e4aa9e4fac9078ae21175e51e04a23877d6c2044765e39959e9a6a3ede99d08a556c196f51a2be12117681b1ef9d0b0498fb2fa4e88649ab9403e743504e3aaefbce8c5cb474eef8f4724ccd076fd33857de510d8509b67f166d986443bc262d776ec20985f617a7aa86e490290ce5d66332c8b45742a527";
+ expected_M1 = trim(new BigInteger("7a2f768791abaeb954eb7f001bb60d91e6f61e959c8fcdefb58de857af9edaac", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("d78da7e0a23c9b87a2f09cdee05c510c105b4a8d471b47402c38f4cdfa49fe6d", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 9: username and password *was* failing in localhost testProviderAPI*/
+ username = "oo";
+ password = "password";
+ a = "1322ec50";
+ salt = "a93c74934dcadd90";
+ expected_A = trim(new BigInteger("c8e9f30a2f67977ee7e61e9ca5af8bd854b6cc98fe01dbe1b1a4cf002c1e2523b7e49f373a600ff85a84867817ec60fec532857812a07f5c6189f6172f133023af75ca4cb98b758bb84620d0aa3cfc74dc69e0507114c0aeab5a75c3ae3f07a919c5729420f03266c26ed41d1846e07de023ec68dd6830e9ebf129cf51abb571", 16).toByteArray());
+ x_string = "20470538560c4beb4908e6bfe5b0e00da94223e361302a25c898cbdd3724020";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ B = "24f98ede155212bea8b1d8bacf8153735ee8114faa824c57c84df55f8d6072ab87f5ae885ce1062939dbaa68ca6e63147c1d2dc1f751e8be20d8a6f87287a2a83fcb1dc9b85dd406d438aeee5ccbc873603cb399627e26e6444e94b3d5d26764e866776c8960fe206bd33febeca9f55f6291dd2cb832eab69e5373f548adeefb";
+ expected_M1 = trim(new BigInteger("1b35c705e563bd5239cdccc6627aa877c3023286f49b4b7c21341d2949ca2d15", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("a382025452bad8a6ccd0f703253fda90e7ea7bd0c2d466a389455080a4bd015d", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 10: derived from test 11, discovered that password bytes should be in ISO-8859-1*/
+ username = "nostradamus";
+ password = "$[[//jjiilajfewahug43a89y¿";
+ a = "800f0819";
+ salt = "cfb9ae3ec5433076";
+ expected_A = trim(new BigInteger("2ab09ee2fa01058f2f72fd2142b129f2ec26313801052889bcc4af57ee2e4d5b92c90cdfd6ecd660e82c635b2a091ba1b164e5b371c911ce0c4e69686baa120c58e2e0af84b2adc10da6cdfb0b579a1685032c57fd6ed1306d9713a562eddf5c833725042e825fa1abc7017f74760cb53d8c755ffe628c510022c296d1cd3584", 16).toByteArray());
+ x_string = "9736a5e386a18f35bb08cac0f7c70bdbe120f2efe019874d0eb23b85b1955858";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ B = "2d19fe17dca1bda01044a0f406547895c32a10df2b0e69676de911273a8685d294763c4d16b3663f722b8980126e2c659efd33ffc6435a9594a2539e726c48e365893b3374670bd1958c13f55c2defa8ea9c0f9ba1345a5dca0e78debba434c8b755353d066d42bc5dfe0403fdcacfe5efd25c685f883ee6766c710b775c50f2";
+ expected_M1 = trim(new BigInteger("a33feada1771c6f53e9343f5b9e69d51d4f15043c95fb663b6dd5b1c7af6f66b", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("9e99f9adfbfaa7add3626ed6e6aea94c9fa60dab6b8d56ad0cc950548f577d32", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+
+ /* Test 11: username and password failing in localhost testProviderAPI*/
+ username = "nostradamus";
+ password = "$[['//\"jjiilajfewahug43a89y¿";
+ a = "5bfbc968";
+ salt = "1bcae1065951bbf5";
+ expected_A = trim(new BigInteger("7a74c254d46dd6010a7090e574817a03f32ba13f98ed3c695d96f09c9d334e591771541400e68b6d27a19e734baccf3965ca79c0294ffbf553716b41fbca627c7cd3ea4a0d1c640c22411881696f59ad7ed8ce6ef7010e43f57fb3858aa4c3479dd41e4073afadb6a516c41f649b8cf30dea6366efa711c5106c83ea71b00da4", 16).toByteArray());
+ x_string = "9834210874c883db35785ee6648079e13d22450c472d6469192ea775ff50c646";
+ expected_x = trim(new BigInteger(x_string, 16).toByteArray());
+ assertEquals(new BigInteger(1, expected_x).toString(16), x_string);
+ B = "285b00c034da5676dd8938ce6a7b717968fef2e5f479ecca6d95828a6ce809dd37893752c956245b5d13315987c50e57cc68aa4f770ff9ce977ddfd65052f278b90545286cf32b3d18307140514e0fe2269fc0437fb16104358f6fa127dc97281a017582759644862d736f48025f2b35cb1662067c11f2fcf0753e2f72c9e028";
+ expected_M1 = trim(new BigInteger("fedbaff9d9a19efc4eea949b045297a6a3121cf371e2acdda85a2a1ca61c929d", 16).toByteArray());
+ expected_M2 = trim(new BigInteger("ffccafa0febc1771a428082b30b7ce409856de4581c7d7d986f5b80015aba0d3", 16).toByteArray());
+
+ a_byte = new BigInteger(a, 16).toByteArray();
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), new BigInteger("2").toByteArray(), new BigInteger(salt, 16).toByteArray(), "SHA-256");
+ client = new LeapSRPSession(username, password, params, a_byte);
+
+ x = client.calculatePasswordHash(username, password, trim(new BigInteger(salt, 16).toByteArray()));
+ assertTrue(Arrays.equals(x, expected_x));
+ assertEquals(new BigInteger(1, expected_x).toString(16), new BigInteger(1, x).toString(16));
+
+ A = client.exponential();
+ assertTrue(Arrays.equals(A, expected_A));
+
+ M1 = client.response(new BigInteger(salt, 16).toByteArray(), new BigInteger(B, 16).toByteArray());
+
+ assertTrue(Arrays.equals(M1, expected_M1));
+
+ verified = client.verify(expected_M2);
+ assertTrue(verified);
+ }
+
+ public 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;
+ }
+
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..c1f13ba6
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="se.leap.bitmaskclient"
+ android:versionCode="74"
+ android:versionName="0.5" >
+
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+ <!-- if you want to run test, this permissions are needed. Gradle will get rid of them once we implement it. -->
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+
+ <!-- <uses-permission android:name="com.android.vending.BILLING" /> -->
+
+ <uses-sdk
+ android:minSdkVersion="14"
+ android:targetSdkVersion="18" />
+
+ <application
+ android:allowBackup="true"
+ android:icon="@drawable/icon"
+ android:logo="@drawable/icon"
+ android:label="@string/app" >
+
+ <service
+ android:name="se.leap.openvpn.OpenVpnService"
+ android:permission="android.permission.BIND_VPN_SERVICE" >
+ <intent-filter>
+ <action android:name="android.net.VpnService" />
+ </intent-filter>
+ </service>
+ <service android:name="se.leap.bitmaskclient.ProviderAPI" android:enabled="true"/>
+
+ <activity
+ android:name="se.leap.openvpn.LaunchVPN"
+ android:label="@string/vpn_launch_title" >
+ </activity>
+
+ <activity
+ android:name="se.leap.bitmaskclient.Dashboard"
+ android:label="@string/title_activity_dashboard"
+ android:uiOptions="splitActionBarWhenNarrow" >
+
+ <intent-filter android:label="@string/app_name">
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <activity
+ android:name="se.leap.bitmaskclient.ConfigurationWizard"
+ android:label="@string/title_activity_configuration_wizard"
+ android:uiOptions="splitActionBarWhenNarrow" >
+ </activity>
+ <activity
+ android:name="se.leap.bitmaskclient.AboutActivity"
+ android:label="@string/title_about_activity" >
+ </activity>
+
+ <service android:name="se.leap.bitmaskclient.EIP" android:exported="false">
+ <intent-filter>
+ <action android:name="se.leap.bitmaskclient.UPDATE_EIP_SERVICE"/>
+ <action android:name="se.leap.bitmaskclient.START_EIP"/>
+ <action android:name="se.leap.bitmaskclient.STOP_EIP"/>
+ </intent-filter>
+ </service>
+ </application>
+
+</manifest>
diff --git a/app/src/main/assets/minivpn.armeabi b/app/src/main/assets/minivpn.armeabi
new file mode 100644
index 00000000..7018dbc9
--- /dev/null
+++ b/app/src/main/assets/minivpn.armeabi
Binary files differ
diff --git a/app/src/main/assets/minivpn.armeabi-v7a b/app/src/main/assets/minivpn.armeabi-v7a
new file mode 100644
index 00000000..a8e01017
--- /dev/null
+++ b/app/src/main/assets/minivpn.armeabi-v7a
Binary files differ
diff --git a/app/src/main/assets/minivpn.mips b/app/src/main/assets/minivpn.mips
new file mode 100644
index 00000000..c44e56c5
--- /dev/null
+++ b/app/src/main/assets/minivpn.mips
Binary files differ
diff --git a/app/src/main/assets/minivpn.x86 b/app/src/main/assets/minivpn.x86
new file mode 100644
index 00000000..1a6bf464
--- /dev/null
+++ b/app/src/main/assets/minivpn.x86
Binary files differ
diff --git a/app/src/main/assets/urls/bitmask demo.url b/app/src/main/assets/urls/bitmask demo.url
new file mode 100644
index 00000000..1a412055
--- /dev/null
+++ b/app/src/main/assets/urls/bitmask demo.url
@@ -0,0 +1,3 @@
+{
+ "main_url" : "https://demo.bitmask.net/"
+}
diff --git a/app/src/main/java/org/jboss/security/srp/SRPParameters.java b/app/src/main/java/org/jboss/security/srp/SRPParameters.java
new file mode 100644
index 00000000..4b188cb3
--- /dev/null
+++ b/app/src/main/java/org/jboss/security/srp/SRPParameters.java
@@ -0,0 +1,150 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2006, Red Hat Middleware LLC, and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.jboss.security.srp;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+import org.spongycastle.util.encoders.Base64;
+
+/** The RFC2945 algorithm session parameters that the client and server
+agree to use. In addition to the base RFC2945 parameters, one can choose an
+alternate hash algorithm for the private session key.
+
+@author Scott.Stark@jboss.org
+@version $Revision: 57210 $
+*/
+public class SRPParameters implements Cloneable, Serializable
+{
+ /** The serial version ID.
+ * @since 1.2.4.1
+ */
+ private static final long serialVersionUID = 6438772808805276693L;
+
+ /** The algorithm safe-prime modulus */
+ public final byte[] N;
+ /** The algorithm primitive generator */
+ public final byte[] g;
+ /** The random password salt originally used to verify the password */
+ public final byte[] s;
+ /** The algorithm to hash the session key to produce K. To be consistent
+ with the RFC2945 description this must be SHA_Interleave as implemented
+ by the JBossSX security provider. For compatibility with earlier JBossSX
+ SRP releases the algorithm must be SHA_ReverseInterleave. This name is
+ passed to java.security.MessageDigest.getInstance(). */
+ public final String hashAlgorithm;
+ /** The algorithm to use for any encryption of data.
+ */
+ public final String cipherAlgorithm;
+ /** The cipher intialization vector bytes
+ */
+ public byte[] cipherIV;
+
+ /** Creates a new instance of SRPParameters */
+ public SRPParameters(byte[] N, byte[] g, byte[] s)
+ {
+ this(N, g, s, "SHA_Interleave", null);
+ }
+ public SRPParameters(byte[] N, byte[] g, byte[] s, String hashAlgorithm)
+ {
+ this(N, g, s, hashAlgorithm, null);
+ }
+ public SRPParameters(byte[] N, byte[] g, byte[] s, String hashAlgorithm,
+ String cipherAlgorithm)
+ {
+ this(N, g, s, hashAlgorithm, cipherAlgorithm, null);
+ }
+ public SRPParameters(byte[] N, byte[] g, byte[] s, String hashAlgorithm,
+ String cipherAlgorithm, byte[] cipherIV)
+ {
+ this.N = N;
+ this.g = g;
+ this.s = s;
+ if( hashAlgorithm == null )
+ hashAlgorithm = "SHA_Interleave";
+ this.hashAlgorithm = hashAlgorithm;
+ this.cipherAlgorithm = cipherAlgorithm;
+ this.cipherIV = cipherIV;
+ }
+
+ public Object clone()
+ {
+ Object clone = null;
+ try
+ {
+ clone = super.clone();
+ }
+ catch(CloneNotSupportedException e)
+ {
+ }
+ return clone;
+ }
+
+ public int hashCode()
+ {
+ int hashCode = hashAlgorithm.hashCode();
+ for(int i = 0; i < N.length; i ++)
+ hashCode += N[i];
+ for(int i = 0; i < g.length; i ++)
+ hashCode += g[i];
+ for(int i = 0; i < s.length; i ++)
+ hashCode += s[i];
+ return hashCode;
+ }
+
+ public boolean equals(Object obj)
+ {
+ boolean equals = false;
+ if( obj instanceof SRPParameters )
+ {
+ SRPParameters p = (SRPParameters) obj;
+ equals = hashAlgorithm.equals(p.hashAlgorithm);
+ if( equals == true )
+ equals = Arrays.equals(N, p.N);
+ if( equals == true )
+ equals = Arrays.equals(g, p.g);
+ if( equals == true )
+ equals = Arrays.equals(s, p.s);
+ }
+ return equals;
+ }
+
+ public String toString()
+ {
+ StringBuffer tmp = new StringBuffer(super.toString());
+ tmp.append('{');
+ tmp.append("N: ");
+ tmp.append(Base64.encode(N));
+ tmp.append("|g: ");
+ tmp.append(Base64.encode(g));
+ tmp.append("|s: ");
+ tmp.append(Base64.encode(s));
+ tmp.append("|hashAlgorithm: ");
+ tmp.append(hashAlgorithm);
+ tmp.append("|cipherAlgorithm: ");
+ tmp.append(cipherAlgorithm);
+ tmp.append("|cipherIV: ");
+ tmp.append(cipherIV);
+ tmp.append('}');
+ return tmp.toString();
+ }
+}
diff --git a/app/src/main/java/org/spongycastle/util/encoders/Base64.java b/app/src/main/java/org/spongycastle/util/encoders/Base64.java
new file mode 100644
index 00000000..87bd80a0
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/encoders/Base64.java
@@ -0,0 +1,121 @@
+package org.spongycastle.util.encoders;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class Base64
+{
+ private static final Encoder encoder = new Base64Encoder();
+
+ /**
+ * encode the input data producing a base 64 encoded byte array.
+ *
+ * @return a byte array containing the base 64 encoded data.
+ */
+ public static byte[] encode(
+ byte[] data)
+ {
+ int len = (data.length + 2) / 3 * 4;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.encode(data, 0, data.length, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception encoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * Encode the byte data to base 64 writing it to the given output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int encode(
+ byte[] data,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.encode(data, 0, data.length, out);
+ }
+
+ /**
+ * Encode the byte data to base 64 writing it to the given output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int encode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.encode(data, off, length, out);
+ }
+
+ /**
+ * decode the base 64 encoded input data. It is assumed the input data is valid.
+ *
+ * @return a byte array representing the decoded data.
+ */
+ public static byte[] decode(
+ byte[] data)
+ {
+ int len = data.length / 4 * 3;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.decode(data, 0, data.length, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception decoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * decode the base 64 encoded String data - whitespace will be ignored.
+ *
+ * @return a byte array representing the decoded data.
+ */
+ public static byte[] decode(
+ String data)
+ {
+ int len = data.length() / 4 * 3;
+ ByteArrayOutputStream bOut = new ByteArrayOutputStream(len);
+
+ try
+ {
+ encoder.decode(data, bOut);
+ }
+ catch (IOException e)
+ {
+ throw new RuntimeException("exception decoding base64 string: " + e);
+ }
+
+ return bOut.toByteArray();
+ }
+
+ /**
+ * decode the base 64 encoded String data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public static int decode(
+ String data,
+ OutputStream out)
+ throws IOException
+ {
+ return encoder.decode(data, out);
+ }
+}
diff --git a/app/src/main/java/org/spongycastle/util/encoders/Base64Encoder.java b/app/src/main/java/org/spongycastle/util/encoders/Base64Encoder.java
new file mode 100644
index 00000000..84060707
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/encoders/Base64Encoder.java
@@ -0,0 +1,298 @@
+package org.spongycastle.util.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class Base64Encoder
+ implements Encoder
+{
+ protected final byte[] encodingTable =
+ {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v',
+ (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6',
+ (byte)'7', (byte)'8', (byte)'9',
+ (byte)'+', (byte)'/'
+ };
+
+ protected byte padding = (byte)'=';
+
+ /*
+ * set up the decoding table.
+ */
+ protected final byte[] decodingTable = new byte[128];
+
+ protected void initialiseDecodingTable()
+ {
+ for (int i = 0; i < encodingTable.length; i++)
+ {
+ decodingTable[encodingTable[i]] = (byte)i;
+ }
+ }
+
+ public Base64Encoder()
+ {
+ initialiseDecodingTable();
+ }
+
+ /**
+ * encode the input data producing a base 64 output stream.
+ *
+ * @return the number of bytes produced.
+ */
+ public int encode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ int modulus = length % 3;
+ int dataLength = (length - modulus);
+ int a1, a2, a3;
+
+ for (int i = off; i < off + dataLength; i += 3)
+ {
+ a1 = data[i] & 0xff;
+ a2 = data[i + 1] & 0xff;
+ a3 = data[i + 2] & 0xff;
+
+ out.write(encodingTable[(a1 >>> 2) & 0x3f]);
+ out.write(encodingTable[((a1 << 4) | (a2 >>> 4)) & 0x3f]);
+ out.write(encodingTable[((a2 << 2) | (a3 >>> 6)) & 0x3f]);
+ out.write(encodingTable[a3 & 0x3f]);
+ }
+
+ /*
+ * process the tail end.
+ */
+ int b1, b2, b3;
+ int d1, d2;
+
+ switch (modulus)
+ {
+ case 0: /* nothing left to do */
+ break;
+ case 1:
+ d1 = data[off + dataLength] & 0xff;
+ b1 = (d1 >>> 2) & 0x3f;
+ b2 = (d1 << 4) & 0x3f;
+
+ out.write(encodingTable[b1]);
+ out.write(encodingTable[b2]);
+ out.write(padding);
+ out.write(padding);
+ break;
+ case 2:
+ d1 = data[off + dataLength] & 0xff;
+ d2 = data[off + dataLength + 1] & 0xff;
+
+ b1 = (d1 >>> 2) & 0x3f;
+ b2 = ((d1 << 4) | (d2 >>> 4)) & 0x3f;
+ b3 = (d2 << 2) & 0x3f;
+
+ out.write(encodingTable[b1]);
+ out.write(encodingTable[b2]);
+ out.write(encodingTable[b3]);
+ out.write(padding);
+ break;
+ }
+
+ return (dataLength / 3) * 4 + ((modulus == 0) ? 0 : 4);
+ }
+
+ private boolean ignore(
+ char c)
+ {
+ return (c == '\n' || c =='\r' || c == '\t' || c == ' ');
+ }
+
+ /**
+ * decode the base 64 encoded byte data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public int decode(
+ byte[] data,
+ int off,
+ int length,
+ OutputStream out)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+ int outLen = 0;
+
+ int end = off + length;
+
+ while (end > off)
+ {
+ if (!ignore((char)data[end - 1]))
+ {
+ break;
+ }
+
+ end--;
+ }
+
+ int i = off;
+ int finish = end - 4;
+
+ i = nextI(data, i, finish);
+
+ while (i < finish)
+ {
+ b1 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b2 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b3 = decodingTable[data[i++]];
+
+ i = nextI(data, i, finish);
+
+ b4 = decodingTable[data[i++]];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ outLen += 3;
+
+ i = nextI(data, i, finish);
+ }
+
+ outLen += decodeLastBlock(out, (char)data[end - 4], (char)data[end - 3], (char)data[end - 2], (char)data[end - 1]);
+
+ return outLen;
+ }
+
+ private int nextI(byte[] data, int i, int finish)
+ {
+ while ((i < finish) && ignore((char)data[i]))
+ {
+ i++;
+ }
+ return i;
+ }
+
+ /**
+ * decode the base 64 encoded String data writing it to the given output stream,
+ * whitespace characters will be ignored.
+ *
+ * @return the number of bytes produced.
+ */
+ public int decode(
+ String data,
+ OutputStream out)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+ int length = 0;
+
+ int end = data.length();
+
+ while (end > 0)
+ {
+ if (!ignore(data.charAt(end - 1)))
+ {
+ break;
+ }
+
+ end--;
+ }
+
+ int i = 0;
+ int finish = end - 4;
+
+ i = nextI(data, i, finish);
+
+ while (i < finish)
+ {
+ b1 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b2 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b3 = decodingTable[data.charAt(i++)];
+
+ i = nextI(data, i, finish);
+
+ b4 = decodingTable[data.charAt(i++)];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ length += 3;
+
+ i = nextI(data, i, finish);
+ }
+
+ length += decodeLastBlock(out, data.charAt(end - 4), data.charAt(end - 3), data.charAt(end - 2), data.charAt(end - 1));
+
+ return length;
+ }
+
+ private int decodeLastBlock(OutputStream out, char c1, char c2, char c3, char c4)
+ throws IOException
+ {
+ byte b1, b2, b3, b4;
+
+ if (c3 == padding)
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+
+ out.write((b1 << 2) | (b2 >> 4));
+
+ return 1;
+ }
+ else if (c4 == padding)
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+ b3 = decodingTable[c3];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+
+ return 2;
+ }
+ else
+ {
+ b1 = decodingTable[c1];
+ b2 = decodingTable[c2];
+ b3 = decodingTable[c3];
+ b4 = decodingTable[c4];
+
+ out.write((b1 << 2) | (b2 >> 4));
+ out.write((b2 << 4) | (b3 >> 2));
+ out.write((b3 << 6) | b4);
+
+ return 3;
+ }
+ }
+
+ private int nextI(String data, int i, int finish)
+ {
+ while ((i < finish) && ignore(data.charAt(i)))
+ {
+ i++;
+ }
+ return i;
+ }
+}
diff --git a/app/src/main/java/org/spongycastle/util/encoders/Encoder.java b/app/src/main/java/org/spongycastle/util/encoders/Encoder.java
new file mode 100644
index 00000000..106c36b7
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/encoders/Encoder.java
@@ -0,0 +1,17 @@
+package org.spongycastle.util.encoders;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * Encode and decode byte arrays (typically from binary to 7-bit ASCII
+ * encodings).
+ */
+public interface Encoder
+{
+ int encode(byte[] data, int off, int length, OutputStream out) throws IOException;
+
+ int decode(byte[] data, int off, int length, OutputStream out) throws IOException;
+
+ int decode(String data, OutputStream out) throws IOException;
+}
diff --git a/app/src/main/java/org/spongycastle/util/io/pem/PemGenerationException.java b/app/src/main/java/org/spongycastle/util/io/pem/PemGenerationException.java
new file mode 100644
index 00000000..0127ca0c
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/io/pem/PemGenerationException.java
@@ -0,0 +1,26 @@
+package org.spongycastle.util.io.pem;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+public class PemGenerationException
+ extends IOException
+{
+ private Throwable cause;
+
+ public PemGenerationException(String message, Throwable cause)
+ {
+ super(message);
+ this.cause = cause;
+ }
+
+ public PemGenerationException(String message)
+ {
+ super(message);
+ }
+
+ public Throwable getCause()
+ {
+ return cause;
+ }
+}
diff --git a/app/src/main/java/org/spongycastle/util/io/pem/PemHeader.java b/app/src/main/java/org/spongycastle/util/io/pem/PemHeader.java
new file mode 100644
index 00000000..4adb815e
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/io/pem/PemHeader.java
@@ -0,0 +1,66 @@
+package org.spongycastle.util.io.pem;
+
+public class PemHeader
+{
+ private String name;
+ private String value;
+
+ public PemHeader(String name, String value)
+ {
+ this.name = name;
+ this.value = value;
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+
+ public int hashCode()
+ {
+ return getHashCode(this.name) + 31 * getHashCode(this.value);
+ }
+
+ public boolean equals(Object o)
+ {
+ if (!(o instanceof PemHeader))
+ {
+ return false;
+ }
+
+ PemHeader other = (PemHeader)o;
+
+ return other == this || (isEqual(this.name, other.name) && isEqual(this.value, other.value));
+ }
+
+ private int getHashCode(String s)
+ {
+ if (s == null)
+ {
+ return 1;
+ }
+
+ return s.hashCode();
+ }
+
+ private boolean isEqual(String s1, String s2)
+ {
+ if (s1 == s2)
+ {
+ return true;
+ }
+
+ if (s1 == null || s2 == null)
+ {
+ return false;
+ }
+
+ return s1.equals(s2);
+ }
+
+}
diff --git a/app/src/main/java/org/spongycastle/util/io/pem/PemObject.java b/app/src/main/java/org/spongycastle/util/io/pem/PemObject.java
new file mode 100644
index 00000000..6f7c79c5
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/io/pem/PemObject.java
@@ -0,0 +1,62 @@
+package org.spongycastle.util.io.pem;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+@SuppressWarnings("all")
+public class PemObject
+ implements PemObjectGenerator
+{
+ private static final List EMPTY_LIST = Collections.unmodifiableList(new ArrayList());
+
+ private String type;
+ private List headers;
+ private byte[] content;
+
+ /**
+ * Generic constructor for object without headers.
+ *
+ * @param type pem object type.
+ * @param content the binary content of the object.
+ */
+ public PemObject(String type, byte[] content)
+ {
+ this(type, EMPTY_LIST, content);
+ }
+
+ /**
+ * Generic constructor for object with headers.
+ *
+ * @param type pem object type.
+ * @param headers a list of PemHeader objects.
+ * @param content the binary content of the object.
+ */
+ public PemObject(String type, List headers, byte[] content)
+ {
+ this.type = type;
+ this.headers = Collections.unmodifiableList(headers);
+ this.content = content;
+ }
+
+ public String getType()
+ {
+ return type;
+ }
+
+ public List getHeaders()
+ {
+ return headers;
+ }
+
+ public byte[] getContent()
+ {
+ return content;
+ }
+
+ public PemObject generate()
+ throws PemGenerationException
+ {
+ return this;
+ }
+}
diff --git a/app/src/main/java/org/spongycastle/util/io/pem/PemObjectGenerator.java b/app/src/main/java/org/spongycastle/util/io/pem/PemObjectGenerator.java
new file mode 100644
index 00000000..1a8cea6d
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/io/pem/PemObjectGenerator.java
@@ -0,0 +1,7 @@
+package org.spongycastle.util.io.pem;
+
+public interface PemObjectGenerator
+{
+ PemObject generate()
+ throws PemGenerationException;
+}
diff --git a/app/src/main/java/org/spongycastle/util/io/pem/PemWriter.java b/app/src/main/java/org/spongycastle/util/io/pem/PemWriter.java
new file mode 100644
index 00000000..f5a6a363
--- /dev/null
+++ b/app/src/main/java/org/spongycastle/util/io/pem/PemWriter.java
@@ -0,0 +1,138 @@
+package org.spongycastle.util.io.pem;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Iterator;
+
+import org.spongycastle.util.encoders.Base64;
+
+/**
+ * A generic PEM writer, based on RFC 1421
+ */
+@SuppressWarnings("all")
+public class PemWriter
+ extends BufferedWriter
+{
+ private static final int LINE_LENGTH = 64;
+
+ private final int nlLength;
+ private char[] buf = new char[LINE_LENGTH];
+
+ /**
+ * Base constructor.
+ *
+ * @param out output stream to use.
+ */
+ public PemWriter(Writer out)
+ {
+ super(out);
+
+ String nl = System.getProperty("line.separator");
+ if (nl != null)
+ {
+ nlLength = nl.length();
+ }
+ else
+ {
+ nlLength = 2;
+ }
+ }
+
+ /**
+ * Return the number of bytes or characters required to contain the
+ * passed in object if it is PEM encoded.
+ *
+ * @param obj pem object to be output
+ * @return an estimate of the number of bytes
+ */
+ public int getOutputSize(PemObject obj)
+ {
+ // BEGIN and END boundaries.
+ int size = (2 * (obj.getType().length() + 10 + nlLength)) + 6 + 4;
+
+ if (!obj.getHeaders().isEmpty())
+ {
+ for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
+ {
+ PemHeader hdr = (PemHeader)it.next();
+
+ size += hdr.getName().length() + ": ".length() + hdr.getValue().length() + nlLength;
+ }
+
+ size += nlLength;
+ }
+
+ // base64 encoding
+ int dataLen = ((obj.getContent().length + 2) / 3) * 4;
+
+ size += dataLen + (((dataLen + LINE_LENGTH - 1) / LINE_LENGTH) * nlLength);
+
+ return size;
+ }
+
+ public void writeObject(PemObjectGenerator objGen)
+ throws IOException
+ {
+ PemObject obj = objGen.generate();
+
+ writePreEncapsulationBoundary(obj.getType());
+
+ if (!obj.getHeaders().isEmpty())
+ {
+ for (Iterator it = obj.getHeaders().iterator(); it.hasNext();)
+ {
+ PemHeader hdr = (PemHeader)it.next();
+
+ this.write(hdr.getName());
+ this.write(": ");
+ this.write(hdr.getValue());
+ this.newLine();
+ }
+
+ this.newLine();
+ }
+
+ writeEncoded(obj.getContent());
+ writePostEncapsulationBoundary(obj.getType());
+ }
+
+ private void writeEncoded(byte[] bytes)
+ throws IOException
+ {
+ bytes = Base64.encode(bytes);
+
+ for (int i = 0; i < bytes.length; i += buf.length)
+ {
+ int index = 0;
+
+ while (index != buf.length)
+ {
+ if ((i + index) >= bytes.length)
+ {
+ break;
+ }
+ buf[index] = (char)bytes[i + index];
+ index++;
+ }
+ this.write(buf, 0, index);
+ this.newLine();
+ }
+ }
+
+ private void writePreEncapsulationBoundary(
+ String type)
+ throws IOException
+ {
+ this.write("-----BEGIN " + type + "-----");
+ this.newLine();
+ }
+
+ private void writePostEncapsulationBoundary(
+ String type)
+ throws IOException
+ {
+ this.write("-----END " + type + "-----");
+ this.newLine();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/AboutActivity.java b/app/src/main/java/se/leap/bitmaskclient/AboutActivity.java
new file mode 100644
index 00000000..6d025422
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/AboutActivity.java
@@ -0,0 +1,40 @@
+package se.leap.bitmaskclient;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import se.leap.bitmaskclient.R;
+
+public class AboutActivity extends Activity {
+
+ final public static String TAG = "aboutFragment";
+ final public static int VIEWED = 0;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.about);
+ TextView ver = (TextView) findViewById(R.id.version);
+
+ String version;
+ String name="Openvpn";
+ try {
+ PackageInfo packageinfo = getPackageManager().getPackageInfo(getPackageName(), 0);
+ version = packageinfo.versionName;
+ name = getString(R.string.app);
+ } catch (NameNotFoundException e) {
+ version = "error fetching version";
+ }
+
+
+ ver.setText(getString(R.string.version_info,name,version));
+ setResult(VIEWED);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
new file mode 100644
index 00000000..a8bd3b7a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.io.InputStream;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Base64;
+
+/**
+ * Stores constants, and implements auxiliary methods used across all LEAP Android classes.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ *
+ */
+public class ConfigHelper {
+ private static KeyStore keystore_trusted;
+
+ final public static String NG_1024 =
+ "eeaf0ab9adb38dd69c33f80afa8fc5e86072618775ff3c0b9ea2314c9c256576d674df7496ea81d3383b4813d692c6e0e0d5d8e250b98be48e495c1d6089dad15dc7d7b46154d6b6ce8ef4ad69b15d4982559b297bcf1885c529f566660e57ec68edbc3c05726cc02fd4cbf4976eaa9afd5138fe8376435b9fc61d2fc0eb06e3";
+ final public static BigInteger G = new BigInteger("2");
+
+ public static boolean checkErroneousDownload(String downloaded_string) {
+ try {
+ if(new JSONObject(downloaded_string).has(ProviderAPI.ERRORS) || downloaded_string.isEmpty()) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch(JSONException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Treat the input as the MSB representation of a number,
+ * and lop off leading zero elements. For efficiency, the
+ * input is simply returned if no leading zeroes are found.
+ *
+ * @param in array to be trimmed
+ */
+ public static byte[] trim(byte[] in) {
+ if(in.length == 0 || in[0] != 0)
+ return in;
+
+ int len = in.length;
+ int i = 1;
+ while(in[i] == 0 && i < len)
+ ++i;
+ byte[] ret = new byte[len - i];
+ System.arraycopy(in, i, ret, 0, len - i);
+ return ret;
+ }
+
+ public static X509Certificate parseX509CertificateFromString(String certificate_string) {
+ java.security.cert.Certificate certificate = null;
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+
+ certificate_string = certificate_string.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim();
+ byte[] cert_bytes = Base64.decode(certificate_string, Base64.DEFAULT);
+ InputStream caInput = new ByteArrayInputStream(cert_bytes);
+ try {
+ certificate = cf.generateCertificate(caInput);
+ System.out.println("ca=" + ((X509Certificate) certificate).getSubjectDN());
+ } finally {
+ caInput.close();
+ }
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ return null;
+ }
+
+ return (X509Certificate) certificate;
+ }
+
+ protected static RSAPrivateKey parseRsaKeyFromString(String RsaKeyString) {
+ RSAPrivateKey key = null;
+ try {
+ KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
+
+ RsaKeyString = RsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec( Base64.decode(RsaKeyString, Base64.DEFAULT) );
+ key = (RSAPrivateKey) kf.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ } catch (NoSuchProviderException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return null;
+ }
+
+ return key;
+ }
+
+ /**
+ * Adds a new X509 certificate given its input stream and its provider name
+ * @param provider used to store the certificate in the keystore
+ * @param inputStream from which X509 certificate must be generated.
+ */
+ public static void addTrustedCertificate(String provider, InputStream inputStream) {
+ CertificateFactory cf;
+ try {
+ cf = CertificateFactory.getInstance("X.509");
+ X509Certificate cert =
+ (X509Certificate)cf.generateCertificate(inputStream);
+ keystore_trusted.setCertificateEntry(provider, cert);
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Adds a new X509 certificate given in its string from and using its provider name
+ * @param provider used to store the certificate in the keystore
+ * @param certificate
+ */
+ public static void addTrustedCertificate(String provider, String certificate) {
+
+ try {
+ X509Certificate cert = ConfigHelper.parseX509CertificateFromString(certificate);
+ if(keystore_trusted == null) {
+ keystore_trusted = KeyStore.getInstance("BKS");
+ keystore_trusted.load(null);
+ }
+ keystore_trusted.setCertificateEntry(provider, cert);
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @return class wide keystore
+ */
+ public static KeyStore getKeystore() {
+ return keystore_trusted;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java
new file mode 100644
index 00000000..f0aac40b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigurationWizard.java
@@ -0,0 +1,581 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+
+
+
+
+
+import android.app.Activity;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.res.AssetManager;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Display;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View.MeasureSpec;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.ProgressBar;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Iterator;
+import org.json.JSONException;
+import org.json.JSONObject;
+import se.leap.bitmaskclient.DownloadFailedDialog.DownloadFailedDialogInterface;
+import se.leap.bitmaskclient.NewProviderDialog.NewProviderDialogInterface;
+import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver;
+import se.leap.bitmaskclient.ProviderDetailFragment.ProviderDetailFragmentInterface;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import se.leap.bitmaskclient.R;
+
+/**
+ * Activity that builds and shows the list of known available providers.
+ *
+ * It also allows the user to enter custom providers with a button.
+ *
+ * @author parmegv
+ *
+ */
+public class ConfigurationWizard extends Activity
+implements ProviderListFragment.Callbacks, NewProviderDialogInterface, ProviderDetailFragmentInterface, DownloadFailedDialogInterface, Receiver {
+
+ private ProgressBar mProgressBar;
+ private TextView progressbar_description;
+ private ProviderListFragment provider_list_fragment;
+ private Intent mConfigState = new Intent();
+
+ final public static String TAG = "se.leap.bitmaskclient.ConfigurationWizard";
+ final public static String TYPE_OF_CERTIFICATE = "type_of_certificate";
+ final public static String ANON_CERTIFICATE = "anon_certificate";
+ final public static String AUTHED_CERTIFICATE = "authed_certificate";
+
+ final protected static String PROVIDER_SET = "PROVIDER SET";
+ final protected static String SERVICES_RETRIEVED = "SERVICES RETRIEVED";
+
+ public ProviderAPIResultReceiver providerAPI_result_receiver;
+ private ProviderAPIBroadcastReceiver_Update providerAPI_broadcast_receiver_update;
+
+ private static SharedPreferences preferences;
+ private static boolean setting_up_provider = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE);
+
+ setContentView(R.layout.configuration_wizard_activity);
+ mProgressBar = (ProgressBar) findViewById(R.id.progressbar_configuration_wizard);
+ mProgressBar.setVisibility(ProgressBar.INVISIBLE);
+ progressbar_description = (TextView) findViewById(R.id.progressbar_description);
+ progressbar_description.setVisibility(TextView.INVISIBLE);
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+ providerAPI_broadcast_receiver_update = new ProviderAPIBroadcastReceiver_Update();
+ IntentFilter update_intent_filter = new IntentFilter(ProviderAPI.UPDATE_PROGRESSBAR);
+ update_intent_filter.addCategory(Intent.CATEGORY_DEFAULT);
+ registerReceiver(providerAPI_broadcast_receiver_update, update_intent_filter);
+
+ loadPreseededProviders();
+
+ // Only create our fragments if we're not restoring a saved instance
+ if ( savedInstanceState == null ){
+ // TODO Some welcome screen?
+ // We will need better flow control when we have more Fragments (e.g. user auth)
+ provider_list_fragment = ProviderListFragment.newInstance();
+ Bundle arguments = new Bundle();
+ int configuration_wizard_request_code = getIntent().getIntExtra(Dashboard.REQUEST_CODE, -1);
+ if(configuration_wizard_request_code == Dashboard.SWITCH_PROVIDER) {
+ arguments.putBoolean(ProviderListFragment.SHOW_ALL_PROVIDERS, true);
+ }
+ provider_list_fragment.setArguments(arguments);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.configuration_wizard_layout, provider_list_fragment, ProviderListFragment.TAG)
+ .commit();
+ }
+
+ // TODO: If exposing deep links into your app, handle intents here.
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(providerAPI_broadcast_receiver_update);
+ }
+
+ public void refreshProviderList(int top_padding) {
+ ProviderListFragment new_provider_list_fragment = new ProviderListFragment();
+ Bundle top_padding_bundle = new Bundle();
+ top_padding_bundle.putInt(ProviderListFragment.TOP_PADDING, top_padding);
+ new_provider_list_fragment.setArguments(top_padding_bundle);
+
+ FragmentManager fragmentManager = getFragmentManager();
+ fragmentManager.beginTransaction()
+ .replace(R.id.configuration_wizard_layout, new_provider_list_fragment, ProviderListFragment.TAG)
+ .commit();
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if(resultCode == ProviderAPI.PROVIDER_OK) {
+ mConfigState.setAction(PROVIDER_SET);
+
+ if (preferences.getBoolean(EIP.ALLOWED_ANON, false)){
+ mConfigState.putExtra(SERVICES_RETRIEVED, true);
+ downloadAnonCert();
+ } else {
+ mProgressBar.incrementProgressBy(1);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ setResult(RESULT_OK);
+ showProviderDetails(getCurrentFocus());
+ }
+ } else if(resultCode == ProviderAPI.PROVIDER_NOK) {
+ //refreshProviderList(0);
+ String reason_to_fail = resultData.getString(ProviderAPI.ERRORS);
+ showDownloadFailedDialog(getCurrentFocus(), reason_to_fail);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ preferences.edit().remove(Provider.KEY).commit();
+ setting_up_provider = false;
+ setResult(RESULT_CANCELED, mConfigState);
+ }
+ else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
+ mProgressBar.incrementProgressBy(1);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ //refreshProviderList(0);
+ setResult(RESULT_OK);
+ showProviderDetails(getCurrentFocus());
+ } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) {
+ //refreshProviderList(0);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ progressbar_description.setVisibility(TextView.GONE);
+ //Toast.makeText(getApplicationContext(), R.string.incorrectly_downloaded_certificate_message, Toast.LENGTH_LONG).show();
+ setResult(RESULT_CANCELED, mConfigState);
+ } else if(resultCode == AboutActivity.VIEWED) {
+ // Do nothing, right now
+ // I need this for CW to wait for the About activity to end before going back to Dashboard.
+ }
+ }
+
+ /**
+ * Callback method from {@link ProviderListFragment.Callbacks}
+ * indicating that the item with the given ID was selected.
+ */
+ @Override
+ public void onItemSelected(String id) {
+ //TODO Code 2 pane view
+ // resetOldConnection();
+ ProviderItem selected_provider = getProvider(id);
+ int provider_index = getProviderIndex(id);
+
+
+ startProgressBar(provider_index+1);
+ provider_list_fragment.hideAllBut(provider_index);
+
+ boolean danger_on = true;
+ if(preferences.contains(ProviderItem.DANGER_ON))
+ danger_on = preferences.getBoolean(ProviderItem.DANGER_ON, false);
+ setUpProvider(selected_provider.providerMainUrl(), danger_on);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if(setting_up_provider) {
+ stopSettingUpProvider();
+ } else {
+ usualBackButton();
+ }
+ }
+
+ private void stopSettingUpProvider() {
+ ProviderAPI.stop();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ mProgressBar.setProgress(0);
+ progressbar_description.setVisibility(TextView.GONE);
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit().remove(Provider.KEY).commit();
+ setting_up_provider = false;
+ showAllProviders();
+ }
+
+ private void usualBackButton() {
+ try {
+ boolean is_provider_set_up = new JSONObject(preferences.getString(Provider.KEY, "no provider")) != null ? true : false;
+ boolean is_provider_set_up_truly = new JSONObject(preferences.getString(Provider.KEY, "no provider")).length() != 0 ? true : false;
+ if(!is_provider_set_up || !is_provider_set_up_truly) {
+ askDashboardToQuitApp();
+ } else {
+ setResult(RESULT_OK);
+ }
+ } catch (JSONException e) {
+ askDashboardToQuitApp();
+ super.onBackPressed();
+ e.printStackTrace();
+ }
+ super.onBackPressed();
+ }
+ private void askDashboardToQuitApp() {
+ Intent ask_quit = new Intent();
+ ask_quit.putExtra(Dashboard.ACTION_QUIT, Dashboard.ACTION_QUIT);
+ setResult(RESULT_CANCELED, ask_quit);
+ }
+
+ private ProviderItem getProvider(String name) {
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ if(provider.name().equalsIgnoreCase(name)) {
+ return provider;
+ }
+ }
+ return null;
+ }
+
+ private String getId(String provider_main_url_string) {
+ try {
+ URL provider_url = new URL(provider_main_url_string);
+ URL aux_provider_url;
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ aux_provider_url = new URL(provider.providerMainUrl());
+ if(isSameURL(provider_url, aux_provider_url)) {
+ return provider.name();
+ }
+ }
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ /**
+ * Checks, whether 2 urls are pointing to the same location.
+ *
+ * @param url a url
+ * @param baseUrl an other url, that should be compared.
+ * @return true, if the urls point to the same host and port and use the
+ * same protocol, false otherwise.
+ */
+ private boolean isSameURL(final URL url, final URL baseUrl) {
+ if (!url.getProtocol().equals(baseUrl.getProtocol())) {
+ return false;
+ }
+ if (!url.getHost().equals(baseUrl.getHost())) {
+ return false;
+ }
+ if (url.getPort() != baseUrl.getPort()) {
+ return false;
+ }
+ return true;
+ }
+
+ private void startProgressBar() {
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ mProgressBar.setProgress(0);
+ mProgressBar.setMax(3);
+ }
+
+ private void startProgressBar(int list_item_index) {
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ progressbar_description.setVisibility(TextView.VISIBLE);
+ mProgressBar.setProgress(0);
+ mProgressBar.setMax(3);
+ int measured_height = listItemHeight(list_item_index);
+ mProgressBar.setTranslationY(measured_height);
+ progressbar_description.setTranslationY(measured_height + mProgressBar.getHeight());
+ }
+
+ private int getProviderIndex(String id) {
+ int index = 0;
+ Iterator<ProviderItem> providers_iterator = ProviderListContent.ITEMS.iterator();
+ while(providers_iterator.hasNext()) {
+ ProviderItem provider = providers_iterator.next();
+ if(provider.name().equalsIgnoreCase(id)) {
+ break;
+ } else index++;
+ }
+ return index;
+ }
+
+ private int listItemHeight(int list_item_index) {
+ ListView provider_list_view = (ListView)findViewById(android.R.id.list);
+ ListAdapter provider_list_adapter = provider_list_view.getAdapter();
+ View listItem = provider_list_adapter.getView(0, null, provider_list_view);
+ listItem.setLayoutParams(new RelativeLayout.LayoutParams(
+ RelativeLayout.LayoutParams.WRAP_CONTENT,
+ RelativeLayout.LayoutParams.WRAP_CONTENT));
+ WindowManager wm = (WindowManager) getApplicationContext()
+ .getSystemService(Context.WINDOW_SERVICE);
+ Display display = wm.getDefaultDisplay();
+ int screenWidth = display.getWidth(); // deprecated
+
+ int listViewWidth = screenWidth - 10 - 10;
+ int widthSpec = MeasureSpec.makeMeasureSpec(listViewWidth,
+ MeasureSpec.AT_MOST);
+ listItem.measure(widthSpec, 0);
+
+ return listItem.getMeasuredHeight();
+}
+
+ /**
+ * Loads providers data from url file contained in the project
+ * @return true if the file was read correctly
+ */
+ private boolean loadPreseededProviders() {
+ boolean loaded_preseeded_providers = false;
+ AssetManager asset_manager = getAssets();
+ String[] urls_filepaths = null;
+ try {
+ String url_files_folder = "urls";
+ //TODO Put that folder in a better place (also inside the "for")
+ urls_filepaths = asset_manager.list(url_files_folder);
+ String provider_name = "";
+ for(String url_filepath : urls_filepaths)
+ {
+ boolean custom = false;
+ provider_name = url_filepath.subSequence(0, url_filepath.indexOf(".")).toString();
+ if(ProviderListContent.ITEMS.isEmpty()) //TODO I have to implement a way of checking if a provider new or is already present in that ITEMS list
+ ProviderListContent.addItem(new ProviderItem(provider_name, asset_manager.open(url_files_folder + "/" + url_filepath)));
+ loaded_preseeded_providers = true;
+ }
+ } catch (IOException e) {
+ loaded_preseeded_providers = false;
+ }
+
+ return loaded_preseeded_providers;
+ }
+
+ /**
+ * Asks ProviderAPI to download an anonymous (anon) VPN certificate.
+ */
+ private void downloadAnonCert() {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+
+ parameters.putString(TYPE_OF_CERTIFICATE, ANON_CERTIFICATE);
+
+ provider_API_command.setAction(ProviderAPI.DOWNLOAD_CERTIFICATE);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ /**
+ * Open the new provider dialog
+ */
+ public void addAndSelectNewProvider() {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG);
+ if (previous_new_provider_dialog != null) {
+ fragment_transaction.remove(previous_new_provider_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = NewProviderDialog.newInstance();
+ newFragment.show(fragment_transaction, NewProviderDialog.TAG);
+ }
+
+ /**
+ * Open the new provider dialog with data
+ */
+ public void addAndSelectNewProvider(String main_url, boolean danger_on) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_new_provider_dialog = getFragmentManager().findFragmentByTag(NewProviderDialog.TAG);
+ if (previous_new_provider_dialog != null) {
+ fragment_transaction.remove(previous_new_provider_dialog);
+ }
+
+ DialogFragment newFragment = NewProviderDialog.newInstance();
+ Bundle data = new Bundle();
+ data.putString(Provider.MAIN_URL, main_url);
+ data.putBoolean(ProviderItem.DANGER_ON, danger_on);
+ newFragment.setArguments(data);
+ newFragment.show(fragment_transaction, NewProviderDialog.TAG);
+ }
+
+ /**
+ * Once selected a provider, this fragment offers the user to log in,
+ * use it anonymously (if possible)
+ * or cancel his/her election pressing the back button.
+ * @param view
+ * @param reason_to_fail
+ */
+ public void showDownloadFailedDialog(View view, String reason_to_fail) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(DownloadFailedDialog.TAG);
+ if (previous_provider_details_dialog != null) {
+ fragment_transaction.remove(previous_provider_details_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail);
+ newFragment.show(fragment_transaction, DownloadFailedDialog.TAG);
+ }
+
+ /**
+ * Once selected a provider, this fragment offers the user to log in,
+ * use it anonymously (if possible)
+ * or cancel his/her election pressing the back button.
+ * @param view
+ */
+ public void showProviderDetails(View view) {
+ if(setting_up_provider) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_provider_details_dialog = getFragmentManager().findFragmentByTag(ProviderDetailFragment.TAG);
+ if (previous_provider_details_dialog != null) {
+ fragment_transaction.remove(previous_provider_details_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = ProviderDetailFragment.newInstance();
+ newFragment.show(fragment_transaction, ProviderDetailFragment.TAG);
+ }
+ }
+
+ public void showAndSelectProvider(String provider_main_url, boolean danger_on) {
+ if(getId(provider_main_url).isEmpty())
+ showProvider(provider_main_url, danger_on);
+ autoSelectProvider(provider_main_url, danger_on);
+ }
+
+ private void showProvider(final String provider_main_url, final boolean danger_on) {
+ String provider_name = provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("\\/", "_");
+ ProviderItem added_provider = new ProviderItem(provider_name, provider_main_url);
+ provider_list_fragment.addItem(added_provider);
+ }
+
+ private void autoSelectProvider(String provider_main_url, boolean danger_on) {
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(ProviderItem.DANGER_ON, danger_on).commit();
+ onItemSelected(getId(provider_main_url));
+ }
+
+ /**
+ * Asks ProviderAPI to download a new provider.json file
+ * @param provider_name
+ * @param provider_main_url
+ * @param danger_on tells if HTTPS client should bypass certificate errors
+ */
+ public void setUpProvider(String provider_main_url, boolean danger_on) {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+ Bundle parameters = new Bundle();
+ parameters.putString(Provider.MAIN_URL, provider_main_url);
+ parameters.putBoolean(ProviderItem.DANGER_ON, danger_on);
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ setting_up_provider = true;
+ }
+
+ public void retrySetUpProvider() {
+ cancelSettingUpProvider();
+ if(!ProviderAPI.caCertDownloaded()) {
+ addAndSelectNewProvider(ProviderAPI.lastProviderMainUrl(), ProviderAPI.lastDangerOn());
+ } else {
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+ }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ switch (item.getItemId()){
+ case R.id.about_leap:
+ startActivityForResult(new Intent(this, AboutActivity.class), 0);
+ return true;
+ case R.id.new_provider:
+ addAndSelectNewProvider();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void showAllProviders() {
+ provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG);
+ if(provider_list_fragment != null)
+ provider_list_fragment.unhideAll();
+ }
+
+ public void cancelSettingUpProvider() {
+ provider_list_fragment = (ProviderListFragment) getFragmentManager().findFragmentByTag(ProviderListFragment.TAG);
+ if(provider_list_fragment != null && preferences.contains(ProviderItem.DANGER_ON)) {
+ provider_list_fragment.removeLastItem();
+ }
+ preferences.edit().remove(Provider.KEY).remove(ProviderItem.DANGER_ON).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit();
+ }
+
+ @Override
+ public void login() {
+ Intent ask_login = new Intent();
+ ask_login.putExtra(LogInDialog.VERB, LogInDialog.VERB);
+ setResult(RESULT_OK, ask_login);
+ setting_up_provider = false;
+ finish();
+ }
+
+ @Override
+ public void use_anonymously() {
+ setResult(RESULT_OK);
+ setting_up_provider = false;
+ finish();
+ }
+
+ public class ProviderAPIBroadcastReceiver_Update extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int update = intent.getIntExtra(ProviderAPI.CURRENT_PROGRESS, 0);
+ mProgressBar.setProgress(update);
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
new file mode 100644
index 00000000..b388b84a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderAPIResultReceiver.Receiver;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * The main user facing Activity of LEAP Android, consisting of status, controls,
+ * and access to preferences.
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ * @author parmegv
+ */
+public class Dashboard extends Activity implements LogInDialog.LogInDialogInterface,Receiver {
+
+ protected static final int CONFIGURE_LEAP = 0;
+ protected static final int SWITCH_PROVIDER = 1;
+
+ final public static String SHARED_PREFERENCES = "LEAPPreferences";
+ final public static String ACTION_QUIT = "quit";
+ public static final String REQUEST_CODE = "request_code";
+
+ private ProgressBar mProgressBar;
+ private TextView eipStatus;
+ private static Context app;
+ private static SharedPreferences preferences;
+ private static Provider provider;
+
+ private TextView providerNameTV;
+
+ private boolean authed_eip = false;
+
+ public ProviderAPIResultReceiver providerAPI_result_receiver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ app = this;
+
+ PRNGFixes.apply();
+ // mProgressBar = (ProgressBar) findViewById(R.id.progressbar_dashboard);
+ // mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ // eipStatus = (TextView) findViewById(R.id.eipStatus);
+
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+
+ preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+
+ authed_eip = preferences.getBoolean(EIP.AUTHED_EIP, false);
+ if (preferences.getString(Provider.KEY, "").isEmpty())
+ startActivityForResult(new Intent(this,ConfigurationWizard.class),CONFIGURE_LEAP);
+ else
+ buildDashboard();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data){
+ if ( requestCode == CONFIGURE_LEAP || requestCode == SWITCH_PROVIDER) {
+ // It should be equivalent: if ( (requestCode == CONFIGURE_LEAP) || (data!= null && data.hasExtra(STOP_FIRST))) {
+ if ( resultCode == RESULT_OK ){
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(EIP.PARSED_SERIAL, 0).commit();
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+ Intent updateEIP = new Intent(getApplicationContext(), EIP.class);
+ updateEIP.setAction(EIP.ACTION_UPDATE_EIP_SERVICE);
+ startService(updateEIP);
+ buildDashboard();
+ invalidateOptionsMenu();
+ if(data != null && data.hasExtra(LogInDialog.VERB)) {
+ View view = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
+ logInDialog(view, Bundle.EMPTY);
+ }
+ } else if(resultCode == RESULT_CANCELED && data.hasExtra(ACTION_QUIT)) {
+ finish();
+ } else
+ configErrorDialog();
+ }
+ }
+
+ /**
+ * Dialog shown when encountering a configuration error. Such errors require
+ * reconfiguring LEAP or aborting the application.
+ */
+ private void configErrorDialog() {
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getAppContext());
+ alertBuilder.setTitle(getResources().getString(R.string.setup_error_title));
+ alertBuilder
+ .setMessage(getResources().getString(R.string.setup_error_text))
+ .setCancelable(false)
+ .setPositiveButton(getResources().getString(R.string.setup_error_configure_button), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startActivityForResult(new Intent(getAppContext(),ConfigurationWizard.class),CONFIGURE_LEAP);
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.setup_error_close_button), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ SharedPreferences.Editor prefsEdit = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE).edit();
+ prefsEdit.remove(Provider.KEY).commit();
+ finish();
+ }
+ })
+ .show();
+ }
+
+ /**
+ * Inflates permanent UI elements of the View and contains logic for what
+ * service dependent UI elements to include.
+ */
+ private void buildDashboard() {
+ provider = Provider.getInstance();
+ provider.init( this );
+
+ setContentView(R.layout.client_dashboard);
+
+ providerNameTV = (TextView) findViewById(R.id.providerName);
+ providerNameTV.setText(provider.getDomain());
+ providerNameTV.setTextSize(28);
+
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+
+ FragmentManager fragMan = getFragmentManager();
+ if ( provider.hasEIP()){
+ EipServiceFragment eipFragment = new EipServiceFragment();
+ fragMan.beginTransaction().replace(R.id.servicesCollection, eipFragment, EipServiceFragment.TAG).commit();
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+
+ if(service_description.getBoolean(Provider.ALLOW_REGISTRATION)) {
+ if(authed_eip) {
+ menu.findItem(R.id.login_button).setVisible(false);
+ menu.findItem(R.id.logout_button).setVisible(true);
+ } else {
+ menu.findItem(R.id.login_button).setVisible(true);
+ menu.findItem(R.id.logout_button).setVisible(false);
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.client_dashboard, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item){
+ Intent intent;
+ switch (item.getItemId()){
+ case R.id.about_leap:
+ intent = new Intent(this, AboutActivity.class);
+ startActivity(intent);
+ return true;
+ case R.id.switch_provider:
+ if (Provider.getInstance().hasEIP()){
+ if (getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getBoolean(EIP.AUTHED_EIP, false)){
+ logOut();
+ }
+ eipStop();
+ }
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().remove(Provider.KEY).commit();
+ startActivityForResult(new Intent(this,ConfigurationWizard.class), SWITCH_PROVIDER);
+ return true;
+ case R.id.login_button:
+ View view = ((ViewGroup)findViewById(android.R.id.content)).getChildAt(0);
+ logInDialog(view, Bundle.EMPTY);
+ return true;
+ case R.id.logout_button:
+ logOut();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+
+ }
+
+ @Override
+ public void authenticate(String username, String password) {
+ mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ eipStatus = (TextView) findViewById(R.id.eipStatus);
+
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+ parameters.putString(LogInDialog.USERNAME, username);
+ parameters.putString(LogInDialog.PASSWORD, password);
+
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
+ parameters.putString(Provider.API_URL, provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ provider_API_command.setAction(ProviderAPI.SRP_AUTH);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ eipStatus.setText(R.string.authenticating_message);
+ //mProgressBar.setMax(4);
+ startService(provider_API_command);
+ }
+
+ public void cancelAuthedEipOn() {
+ EipServiceFragment eipFragment = (EipServiceFragment) getFragmentManager().findFragmentByTag(EipServiceFragment.TAG);
+ eipFragment.checkEipSwitch(false);
+ }
+
+ /**
+ * Asks ProviderAPI to log out.
+ */
+ public void logOut() {
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+
+ JSONObject provider_json;
+ try {
+ provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
+ parameters.putString(Provider.API_URL, provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ provider_API_command.setAction(ProviderAPI.LOG_OUT);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ if(mProgressBar == null) mProgressBar = (ProgressBar) findViewById(R.id.eipProgress);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ if(eipStatus == null) eipStatus = (TextView) findViewById(R.id.eipStatus);
+ eipStatus.setText(R.string.logout_message);
+ // eipStatus.setText("Starting to logout");
+
+ startService(provider_API_command);
+ //mProgressBar.setMax(1);
+
+ }
+
+ /**
+ * Shows the log in dialog.
+ * @param view from which the dialog is created.
+ */
+ public void logInDialog(View view, Bundle resultData) {
+ FragmentTransaction fragment_transaction = getFragmentManager().beginTransaction();
+ Fragment previous_log_in_dialog = getFragmentManager().findFragmentByTag(LogInDialog.TAG);
+ if (previous_log_in_dialog != null) {
+ fragment_transaction.remove(previous_log_in_dialog);
+ }
+ fragment_transaction.addToBackStack(null);
+
+ DialogFragment newFragment = LogInDialog.newInstance();
+ if(resultData != null && !resultData.isEmpty()) {
+ newFragment.setArguments(resultData);
+ }
+ newFragment.show(fragment_transaction, LogInDialog.TAG);
+ }
+
+ /**
+ * Asks ProviderAPI to download an authenticated OpenVPN certificate.
+ * @param session_id cookie for the server to allow us to download the certificate.
+ */
+ private void downloadAuthedUserCertificate(/*Cookie session_id*/) {
+ providerAPI_result_receiver = new ProviderAPIResultReceiver(new Handler());
+ providerAPI_result_receiver.setReceiver(this);
+
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ Bundle parameters = new Bundle();
+ parameters.putString(ConfigurationWizard.TYPE_OF_CERTIFICATE, ConfigurationWizard.AUTHED_CERTIFICATE);
+ /*parameters.putString(ConfigHelper.SESSION_ID_COOKIE_KEY, session_id.getName());
+ parameters.putString(ConfigHelper.SESSION_ID_KEY, session_id.getValue());*/
+
+ provider_API_command.setAction(ProviderAPI.DOWNLOAD_CERTIFICATE);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+
+ startService(provider_API_command);
+ }
+
+ @Override
+ public void onReceiveResult(int resultCode, Bundle resultData) {
+ if(resultCode == ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL){
+ String session_id_cookie_key = resultData.getString(ProviderAPI.SESSION_ID_COOKIE_KEY);
+ String session_id_string = resultData.getString(ProviderAPI.SESSION_ID_KEY);
+ setResult(RESULT_OK);
+
+ authed_eip = true;
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+
+ invalidateOptionsMenu();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ changeStatusMessage(resultCode);
+
+ //Cookie session_id = new BasicClientCookie(session_id_cookie_key, session_id_string);
+ downloadAuthedUserCertificate(/*session_id*/);
+ } else if(resultCode == ProviderAPI.SRP_AUTHENTICATION_FAILED) {
+ logInDialog(getCurrentFocus(), resultData);
+ } else if(resultCode == ProviderAPI.LOGOUT_SUCCESSFUL) {
+ authed_eip = false;
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.AUTHED_EIP, authed_eip).commit();
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ mProgressBar.setProgress(0);
+ invalidateOptionsMenu();
+ setResult(RESULT_OK);
+ changeStatusMessage(resultCode);
+
+ } else if(resultCode == ProviderAPI.LOGOUT_FAILED) {
+ setResult(RESULT_CANCELED);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ } else if(resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
+ setResult(RESULT_OK);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ if(EipServiceFragment.isEipSwitchChecked())
+ eipStart();
+ } else if(resultCode == ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE) {
+ setResult(RESULT_CANCELED);
+ changeStatusMessage(resultCode);
+ mProgressBar.setVisibility(ProgressBar.GONE);
+ }
+ }
+
+ private void changeStatusMessage(final int previous_result_code) {
+ // TODO Auto-generated method stub
+ ResultReceiver eip_status_receiver = new ResultReceiver(new Handler()){
+ protected void onReceiveResult(int resultCode, Bundle resultData){
+ super.onReceiveResult(resultCode, resultData);
+ String request = resultData.getString(EIP.REQUEST_TAG);
+ if (request.equalsIgnoreCase(EIP.ACTION_IS_EIP_RUNNING)){
+ if (resultCode == Activity.RESULT_OK){
+
+ switch(previous_result_code){
+ case ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL: eipStatus.setText(R.string.succesful_authentication_message); break;
+ case ProviderAPI.SRP_AUTHENTICATION_FAILED: eipStatus.setText(R.string.authentication_failed_message); break;
+ case ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.authed_secured_status); break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.incorrectly_downloaded_certificate_message); break;
+ case ProviderAPI.LOGOUT_SUCCESSFUL: eipStatus.setText(R.string.anonymous_secured_status); break;
+ case ProviderAPI.LOGOUT_FAILED: eipStatus.setText(R.string.log_out_failed_message); break;
+
+ }
+ }
+ else if(resultCode == Activity.RESULT_CANCELED){
+
+ switch(previous_result_code){
+
+ case ProviderAPI.SRP_AUTHENTICATION_SUCCESSFUL: eipStatus.setText(R.string.succesful_authentication_message); break;
+ case ProviderAPI.SRP_AUTHENTICATION_FAILED: eipStatus.setText(R.string.authentication_failed_message); break;
+ case ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.future_authed_secured_status); break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE: eipStatus.setText(R.string.incorrectly_downloaded_certificate_message); break;
+ case ProviderAPI.LOGOUT_SUCCESSFUL: eipStatus.setText(R.string.future_anonymous_secured_status); break;
+ case ProviderAPI.LOGOUT_FAILED: eipStatus.setText(R.string.log_out_failed_message); break;
+ }
+ }
+ }
+
+ }
+ };
+ eipIsRunning(eip_status_receiver);
+ }
+
+ /**
+ * For retrieving the base application Context in classes that don't extend
+ * Android's Activity class
+ *
+ * @return Application Context as defined by <code>this</code> for Dashboard instance
+ */
+ public static Context getAppContext() {
+ return app;
+ }
+
+
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode) {
+ intent.putExtra(Dashboard.REQUEST_CODE, requestCode);
+ super.startActivityForResult(intent, requestCode);
+ }
+ /**
+ * Send a command to EIP
+ *
+ * @param action A valid String constant from EIP class representing an Intent
+ * filter for the EIP class
+ */
+ private void eipIsRunning(ResultReceiver eip_receiver){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_IS_EIP_RUNNING);
+ eip_intent.putExtra(EIP.RECEIVER_TAG, eip_receiver);
+ startService(eip_intent);
+ }
+
+ /**
+ * Send a command to EIP
+ *
+ */
+ private void eipStop(){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_STOP_EIP);
+ // eip_intent.putExtra(EIP.RECEIVER_TAG, eip_receiver);
+ startService(eip_intent);
+
+ }
+
+ private void eipStart(){
+ Intent eip_intent = new Intent(this, EIP.class);
+ eip_intent.setAction(EIP.ACTION_START_EIP);
+ eip_intent.putExtra(EIP.RECEIVER_TAG, EipServiceFragment.getReceiver());
+ startService(eip_intent);
+
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
new file mode 100644
index 00000000..f78002b0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.NewProviderDialog.NewProviderDialogInterface;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+/**
+ * Implements a dialog to show why a download failed.
+ *
+ * @author parmegv
+ *
+ */
+public class DownloadFailedDialog extends DialogFragment {
+
+ public static String TAG = "downloaded_failed_dialog";
+ private String reason_to_fail;
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(String reason_to_fail) {
+ DownloadFailedDialog dialog_fragment = new DownloadFailedDialog();
+ dialog_fragment.reason_to_fail = reason_to_fail;
+ return dialog_fragment;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+
+ builder.setMessage(reason_to_fail)
+ .setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ interface_with_ConfigurationWizard.retrySetUpProvider();
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_ConfigurationWizard.cancelSettingUpProvider();
+ dialog.dismiss();
+ }
+ });
+
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+ public interface DownloadFailedDialogInterface {
+ public void retrySetUpProvider();
+ public void cancelSettingUpProvider();
+ }
+
+ DownloadFailedDialogInterface interface_with_ConfigurationWizard;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_ConfigurationWizard = (DownloadFailedDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ interface_with_ConfigurationWizard.cancelSettingUpProvider();
+ dialog.dismiss();
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/EIP.java b/app/src/main/java/se/leap/bitmaskclient/EIP.java
new file mode 100644
index 00000000..e773e3b9
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/EIP.java
@@ -0,0 +1,623 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.Vector;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.openvpn.ConfigParser;
+import se.leap.openvpn.ConfigParser.ConfigParseError;
+import se.leap.openvpn.LaunchVPN;
+import se.leap.openvpn.OpenVpnManagementThread;
+import se.leap.openvpn.OpenVpnService;
+import se.leap.openvpn.OpenVpnService.LocalBinder;
+import se.leap.openvpn.ProfileManager;
+import se.leap.openvpn.VpnProfile;
+import android.app.Activity;
+import android.app.IntentService;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.drm.DrmStore.Action;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+/**
+ * EIP is the abstract base class for interacting with and managing the Encrypted
+ * Internet Proxy connection. Connections are started, stopped, and queried through
+ * this IntentService.
+ * Contains logic for parsing eip-service.json from the provider, configuring and selecting
+ * gateways, and controlling {@link .openvpn.OpenVpnService} connections.
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+public final class EIP extends IntentService {
+
+ public final static String AUTHED_EIP = "authed eip";
+ public final static String ACTION_START_EIP = "se.leap.bitmaskclient.START_EIP";
+ public final static String ACTION_STOP_EIP = "se.leap.bitmaskclient.STOP_EIP";
+ public final static String ACTION_UPDATE_EIP_SERVICE = "se.leap.bitmaskclient.UPDATE_EIP_SERVICE";
+ public final static String ACTION_IS_EIP_RUNNING = "se.leap.bitmaskclient.IS_RUNNING";
+ public final static String EIP_NOTIFICATION = "EIP_NOTIFICATION";
+ public final static String ALLOWED_ANON = "allow_anonymous";
+ public final static String CERTIFICATE = "cert";
+ public final static String PRIVATE_KEY = "private_key";
+ public final static String KEY = "eip";
+ public final static String PARSED_SERIAL = "eip_parsed_serial";
+ public final static String SERVICE_API_PATH = "config/eip-service.json";
+ public final static String RECEIVER_TAG = "receiverTag";
+ public final static String REQUEST_TAG = "requestTag";
+ public final static String TAG = "se.leap.bitmaskclient.EIP";
+
+
+ private static Context context;
+ private static ResultReceiver mReceiver;
+ private static OpenVpnService mVpnService;
+ private static boolean mBound = false;
+ // Used to store actions to "resume" onServiceConnection
+ private static String mPending = null;
+
+ private static int parsedEipSerial;
+ private static JSONObject eipDefinition = null;
+
+ private static OVPNGateway activeGateway = null;
+
+ public EIP(){
+ super("LEAPEIP");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ context = getApplicationContext();
+
+ updateEIPService();
+
+ this.retreiveVpnService();
+ }
+
+ @Override
+ public void onDestroy() {
+ unbindService(mVpnServiceConn);
+ mBound = false;
+
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ String action = intent.getAction();
+ mReceiver = intent.getParcelableExtra(RECEIVER_TAG);
+
+ if ( action == ACTION_IS_EIP_RUNNING )
+ this.isRunning();
+ if ( action == ACTION_UPDATE_EIP_SERVICE )
+ this.updateEIPService();
+ else if ( action == ACTION_START_EIP )
+ this.startEIP();
+ else if ( action == ACTION_STOP_EIP )
+ this.stopEIP();
+ }
+
+ /**
+ * Sends an Intent to bind OpenVpnService.
+ * Used when OpenVpnService isn't bound but might be running.
+ */
+ private boolean retreiveVpnService() {
+ Intent bindIntent = new Intent(this,OpenVpnService.class);
+ bindIntent.setAction(OpenVpnService.RETRIEVE_SERVICE);
+ return bindService(bindIntent, mVpnServiceConn, BIND_AUTO_CREATE);
+ }
+
+ private static ServiceConnection mVpnServiceConn = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ LocalBinder binder = (LocalBinder) service;
+ mVpnService = binder.getService();
+ mBound = true;
+
+ if (mReceiver != null && mPending != null) {
+
+ boolean running = mVpnService.isRunning();
+
+ int resultCode = Activity.RESULT_CANCELED;
+
+ if (mPending.equals(ACTION_IS_EIP_RUNNING)){
+ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+
+ }
+ else if (mPending.equals(ACTION_START_EIP)){
+ resultCode = (running) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+ }
+ else if (mPending.equals(ACTION_STOP_EIP)){
+ resultCode = (running) ? Activity.RESULT_CANCELED
+ : Activity.RESULT_OK;
+ }
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING);
+ mReceiver.send(resultCode, resultData);
+
+ mPending = null;
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ mBound = false;
+
+ if (mReceiver != null){
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, EIP_NOTIFICATION);
+ mReceiver.send(Activity.RESULT_CANCELED, resultData);
+ }
+ }
+
+
+ };
+
+ /**
+ * Attempts to determine if OpenVpnService has an established VPN connection
+ * through the bound ServiceConnection. If there is no bound service, this
+ * method will attempt to bind a running OpenVpnService and send
+ * <code>Activity.RESULT_CANCELED</code> to the ResultReceiver that made the
+ * request.
+ * Note: If the request to bind OpenVpnService is successful, the ResultReceiver
+ * will be notified in {@link onServiceConnected()}
+ */
+
+ private void isRunning() {
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_IS_EIP_RUNNING);
+ int resultCode = Activity.RESULT_CANCELED;
+ if (mBound) {
+ resultCode = (mVpnService.isRunning()) ? Activity.RESULT_OK : Activity.RESULT_CANCELED;
+
+ if (mReceiver != null){
+ mReceiver.send(resultCode, resultData);
+ }
+ } else {
+ mPending = ACTION_IS_EIP_RUNNING;
+ boolean retrieved_vpn_service = retreiveVpnService();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ boolean running = false;
+ try {
+ running = mVpnService.isRunning();
+ } catch (NullPointerException e){
+ e.printStackTrace();
+ }
+
+ if (retrieved_vpn_service && running && mReceiver != null){
+ mReceiver.send(Activity.RESULT_OK, resultData);
+ }
+ else{
+ mReceiver.send(Activity.RESULT_CANCELED, resultData);
+ }
+ }
+ }
+
+ /**
+ * Initiates an EIP connection by selecting a gateway and preparing and sending an
+ * Intent to {@link se.leap.openvpn.LaunchVPN}
+ */
+ private void startEIP() {
+ activeGateway = selectGateway();
+
+ Intent intent = new Intent(this,LaunchVPN.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, activeGateway.mVpnProfile.getUUID().toString() );
+ intent.putExtra(LaunchVPN.EXTRA_NAME, activeGateway.mVpnProfile.getName() );
+ intent.putExtra(RECEIVER_TAG, mReceiver);
+ startActivity(intent);
+ mPending = ACTION_START_EIP;
+ }
+
+ /**
+ * Disconnects the EIP connection gracefully through the bound service or forcefully
+ * if there is no bound service. Sends a message to the requesting ResultReceiver.
+ */
+ private void stopEIP() {
+ if (mBound)
+ mVpnService.onRevoke();
+ else
+ OpenVpnManagementThread.stopOpenVPN();
+
+ if (mReceiver != null){
+ Bundle resultData = new Bundle();
+ resultData.putString(REQUEST_TAG, ACTION_STOP_EIP);
+ mReceiver.send(Activity.RESULT_OK, resultData);
+ }
+ }
+
+ /**
+ * Loads eip-service.json from SharedPreferences and calls {@link updateGateways()}
+ * to parse gateway definitions.
+ * TODO Implement API call to refresh eip-service.json from the provider
+ */
+ private void updateEIPService() {
+ try {
+ eipDefinition = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(KEY, ""));
+ parsedEipSerial = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getInt(PARSED_SERIAL, 0);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ if(parsedEipSerial == 0) {
+ // Delete all vpn profiles
+ ProfileManager vpl = ProfileManager.getInstance(context);
+ VpnProfile[] profiles = (VpnProfile[]) vpl.getProfiles().toArray(new VpnProfile[vpl.getProfiles().size()]);
+ for (int current_profile = 0; current_profile < profiles.length; current_profile++){
+ vpl.removeProfile(context, profiles[current_profile]);
+ }
+ }
+ if (eipDefinition.optInt("serial") > parsedEipSerial)
+ updateGateways();
+ }
+
+ /**
+ * Choose a gateway to connect to based on timezone from system locale data
+ *
+ * @return The gateway to connect to
+ */
+ private OVPNGateway selectGateway() {
+ // TODO Remove String arg constructor in favor of findGatewayByName(String)
+
+ Calendar cal = Calendar.getInstance();
+ int localOffset = cal.get(Calendar.ZONE_OFFSET) / 3600000;
+ TreeMap<Integer, Set<String>> offsets = new TreeMap<Integer, Set<String>>();
+ JSONObject locationsObjects = null;
+ Iterator<String> locations = null;
+ try {
+ locationsObjects = eipDefinition.getJSONObject("locations");
+ locations = locationsObjects.keys();
+ } catch (JSONException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+
+ while (locations.hasNext()) {
+ String locationName = locations.next();
+ JSONObject location = null;
+ try {
+ location = locationsObjects.getJSONObject(locationName);
+
+ // Distance along the numberline of Prime Meridian centric, assumes UTC-11 through UTC+12
+ int dist = Math.abs(localOffset - location.optInt("timezone"));
+ // Farther than 12 timezones and it's shorter around the "back"
+ if (dist > 12)
+ dist = 12 - (dist -12); // Well i'll be. Absolute values make equations do funny things.
+
+ Set<String> set = offsets.get(dist);
+ if (set == null) set = new HashSet<String>();
+ set.add(locationName);
+ offsets.put(dist, set);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+
+ String closestLocation = offsets.isEmpty() ? "" : offsets.firstEntry().getValue().iterator().next();
+ JSONArray gateways = null;
+ String chosenHost = null;
+ try {
+ gateways = eipDefinition.getJSONArray("gateways");
+ for (int i = 0; i < gateways.length(); i++) {
+ JSONObject gw = gateways.getJSONObject(i);
+ if ( gw.getString("location").equalsIgnoreCase(closestLocation) || closestLocation.isEmpty()){
+ chosenHost = gw.getString("host");
+ break;
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ return new OVPNGateway(chosenHost);
+ }
+
+ /**
+ * Walk the list of gateways defined in eip-service.json and parse them into
+ * OVPNGateway objects.
+ * TODO Store the OVPNGateways (as Serializable) in SharedPreferences
+ */
+ private void updateGateways(){
+ JSONArray gatewaysDefined = null;
+
+ try {
+ gatewaysDefined = eipDefinition.getJSONArray("gateways");
+ } catch (JSONException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
+ }
+
+ for ( int i=0 ; i < gatewaysDefined.length(); i++ ){
+
+ JSONObject gw = null;
+
+ try {
+ gw = gatewaysDefined.getJSONObject(i);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ try {
+ if ( gw.getJSONObject("capabilities").getJSONArray("transport").toString().contains("openvpn") ){
+ new OVPNGateway(gw);
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putInt(PARSED_SERIAL, eipDefinition.optInt(Provider.API_RETURN_SERIAL)).commit();
+ }
+
+ /**
+ * OVPNGateway provides objects defining gateways and their options and metadata.
+ * Each instance contains a VpnProfile for OpenVPN specific data and member
+ * variables describing capabilities and location
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+ private class OVPNGateway {
+
+ private String TAG = "OVPNGateway";
+
+ private String mName;
+ private VpnProfile mVpnProfile;
+ private JSONObject mGateway;
+ private HashMap<String,Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>();
+
+
+ /**
+ * Attempts to retrieve a VpnProfile by name and build an OVPNGateway around it.
+ * FIXME This needs to become a findGatewayByName() method
+ *
+ * @param name The hostname of the gateway to inflate
+ */
+ private OVPNGateway(String name){
+ mName = name;
+
+ this.loadVpnProfile();
+ }
+
+ private void loadVpnProfile() {
+ ProfileManager vpl = ProfileManager.getInstance(context);
+
+ try {
+ if ( mName == null )
+ mVpnProfile = vpl.getProfiles().iterator().next();
+ else
+ mVpnProfile = vpl.getProfileByName(mName);
+ } catch (NoSuchElementException e) {
+ updateEIPService();
+ this.loadVpnProfile(); // FIXME catch infinite loops
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Build a gateway object from a JSON OpenVPN gateway definition in eip-service.json
+ * and create a VpnProfile belonging to it.
+ *
+ * @param gateway The JSON OpenVPN gateway definition to parse
+ */
+ protected OVPNGateway(JSONObject gateway){
+
+ mGateway = gateway;
+
+ // Currently deletes VpnProfile for host, if there already is one, and builds new
+ ProfileManager vpl = ProfileManager.getInstance(context);
+ Collection<VpnProfile> profiles = vpl.getProfiles();
+ for (Iterator<VpnProfile> it = profiles.iterator(); it.hasNext(); ){
+ VpnProfile p = it.next();
+ try {
+ if ( p.mName.equalsIgnoreCase( gateway.getString("host") ) ){
+ it.remove();
+ vpl.removeProfile(context, p);
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ this.parseOptions();
+ this.createVPNProfile();
+
+ setUniqueProfileName(vpl);
+ vpl.addProfile(mVpnProfile);
+ vpl.saveProfile(context, mVpnProfile);
+ vpl.saveProfileList(context);
+ }
+
+ /**
+ * Attempts to create a unique profile name from the hostname of the gateway
+ *
+ * @param profileManager
+ */
+ private void setUniqueProfileName(ProfileManager profileManager) {
+ int i=0;
+
+ String newname;
+ try {
+ newname = mGateway.getString("host");
+ while(profileManager.getProfileByName(newname)!=null) {
+ i++;
+ if(i==1)
+ newname = getString(R.string.converted_profile);
+ else
+ newname = getString(R.string.converted_profile_i,i);
+ }
+
+ mVpnProfile.mName=newname;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ Log.v(TAG,"Couldn't read gateway name for profile creation!");
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * FIXME This method is really the outline of the refactoring needed in se.leap.openvpn.ConfigParser
+ */
+ private void parseOptions(){
+
+ // FIXME move these to a common API (& version) definition place, like ProviderAPI or ConfigHelper
+ String common_options = "openvpn_configuration";
+ String remote = "ip_address";
+ String ports = "ports";
+ String protos = "protocols";
+ String capabilities = "capabilities";
+ String location_key = "location";
+ String locations = "locations";
+
+ Vector<String> arg = new Vector<String>();
+ Vector<Vector<String>> args = new Vector<Vector<String>>();
+
+ try {
+ JSONObject def = (JSONObject) eipDefinition.get(common_options);
+ Iterator keys = def.keys();
+ Vector<Vector<String>> value = new Vector<Vector<String>>();
+ while ( keys.hasNext() ){
+ String key = keys.next().toString();
+
+ arg.add(key);
+ for ( String word : def.getString(key).split(" ") )
+ arg.add(word);
+ value.add( (Vector<String>) arg.clone() );
+ options.put(key, (Vector<Vector<String>>) value.clone());
+ value.clear();
+ arg.clear();
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ try {
+ arg.add(remote);
+ arg.add(mGateway.getString(remote));
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ args.add((Vector<String>) arg.clone());
+ options.put("remote", (Vector<Vector<String>>) args.clone() );
+ arg.clear();
+ args.clear();
+
+
+
+ try {
+
+ arg.add(location_key);
+ String locationText = "";
+ locationText = eipDefinition.getJSONObject(locations).getJSONObject(mGateway.getString(location_key)).getString("name");
+ arg.add(locationText);
+
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ args.add((Vector<String>) arg.clone());
+ options.put("location", (Vector<Vector<String>>) args.clone() );
+
+ arg.clear();
+ args.clear();
+ JSONArray protocolsJSON = null;
+ arg.add("proto");
+ try {
+ protocolsJSON = mGateway.getJSONObject(capabilities).getJSONArray(protos);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ Vector<String> protocols = new Vector<String>();
+ for ( int i=0; i<protocolsJSON.length(); i++ )
+ protocols.add(protocolsJSON.optString(i));
+ if ( protocols.contains("udp"))
+ arg.add("udp");
+ else if ( protocols.contains("tcp"))
+ arg.add("tcp");
+ args.add((Vector<String>) arg.clone());
+ options.put("proto", (Vector<Vector<String>>) args.clone());
+ arg.clear();
+ args.clear();
+
+
+ String port = null;
+ arg.add("port");
+ try {
+ port = mGateway.getJSONObject(capabilities).getJSONArray(ports).optString(0);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ arg.add(port);
+ args.add((Vector<String>) arg.clone());
+ options.put("port", (Vector<Vector<String>>) args.clone());
+ args.clear();
+ arg.clear();
+ }
+
+ /**
+ * Create and attach the VpnProfile to our gateway object
+ */
+ protected void createVPNProfile(){
+ try {
+ ConfigParser cp = new ConfigParser();
+ cp.setDefinition(options);
+ VpnProfile vp = cp.convertProfile();
+ mVpnProfile = vp;
+ Log.v(TAG,"Created VPNProfile");
+ } catch (ConfigParseError e) {
+ // FIXME We didn't get a VpnProfile! Error handling! and log level
+ Log.v(TAG,"Error createing VPNProfile");
+ e.printStackTrace();
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java b/app/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java
new file mode 100644
index 00000000..b4cb541a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java
@@ -0,0 +1,306 @@
+package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.openvpn.LogWindow;
+import se.leap.openvpn.OpenVPN;
+import se.leap.openvpn.OpenVPN.StateListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.CompoundButton;
+import android.widget.RelativeLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+public class EipServiceFragment extends Fragment implements StateListener, OnCheckedChangeListener {
+
+ protected static final String IS_EIP_PENDING = "is_eip_pending";
+
+ private View eipFragment;
+ private static Switch eipSwitch;
+ private View eipDetail;
+ private TextView eipStatus;
+
+ private boolean eipAutoSwitched = true;
+
+ private boolean mEipStartPending = false;
+
+ private boolean set_switch_off = false;
+
+ private static EIPReceiver mEIPReceiver;
+
+
+ public static String TAG = "se.leap.bitmask.EipServiceFragment";
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+
+ eipFragment = inflater.inflate(R.layout.eip_service_fragment, container, false);
+
+ eipDetail = ((RelativeLayout) eipFragment.findViewById(R.id.eipDetail));
+ eipDetail.setVisibility(View.VISIBLE);
+
+ View eipSettings = eipFragment.findViewById(R.id.eipSettings);
+ eipSettings.setVisibility(View.GONE); // FIXME too!
+
+ if (mEipStartPending)
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.VISIBLE);
+
+ eipStatus = (TextView) eipFragment.findViewById(R.id.eipStatus);
+
+ eipSwitch = (Switch) eipFragment.findViewById(R.id.eipSwitch);
+
+
+ eipSwitch.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ eipAutoSwitched = false;
+ return false;
+ }
+ });
+ eipSwitch.setOnCheckedChangeListener(this);
+
+
+ return eipFragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mEIPReceiver = new EIPReceiver(new Handler());
+
+ if (savedInstanceState != null)
+ mEipStartPending = savedInstanceState.getBoolean(IS_EIP_PENDING);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ OpenVPN.addStateListener(this);
+ if(set_switch_off) {
+ eipSwitch.setChecked(false);
+ set_switch_off = false;
+ }
+ }
+
+ protected void setSwitchOff(boolean value) {
+ set_switch_off = value;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ OpenVPN.removeStateListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putBoolean(IS_EIP_PENDING, mEipStartPending);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (buttonView.equals(eipSwitch) && !eipAutoSwitched){
+ boolean allowed_anon = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).getBoolean(EIP.ALLOWED_ANON, false);
+ String certificate = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).getString(EIP.CERTIFICATE, "");
+ if(allowed_anon || !certificate.isEmpty()) {
+ if (isChecked){
+ mEipStartPending = true;
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.VISIBLE);
+ ((TextView) eipFragment.findViewById(R.id.eipStatus)).setText(R.string.eip_status_start_pending);
+ eipCommand(EIP.ACTION_START_EIP);
+ } else {
+ if (mEipStartPending){
+ AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getActivity());
+ alertBuilder.setTitle(getResources().getString(R.string.eip_cancel_connect_title));
+ alertBuilder
+ .setMessage(getResources().getString(R.string.eip_cancel_connect_text))
+ .setPositiveButton(getResources().getString(R.string.eip_cancel_connect_cancel), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ eipCommand(EIP.ACTION_STOP_EIP);
+ mEipStartPending = false;
+ }
+ })
+ .setNegativeButton(getResources().getString(R.string.eip_cancel_connect_false), new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(true);
+ eipAutoSwitched = false;
+ }
+ })
+ .show();
+ } else {
+ eipCommand(EIP.ACTION_STOP_EIP);
+ }
+ }
+ }
+ else {
+ Dashboard dashboard = (Dashboard)getActivity();
+ Bundle waiting_on_login = new Bundle();
+ waiting_on_login.putBoolean(IS_EIP_PENDING, true);
+ dashboard.logInDialog(getActivity().getCurrentFocus(), waiting_on_login);
+ }
+ }
+ else {
+ if(!eipSwitch.isChecked())
+ eipStatus.setText(R.string.state_noprocess);
+ }
+ eipAutoSwitched = true;
+ }
+
+
+
+ /**
+ * Send a command to EIP
+ *
+ * @param action A valid String constant from EIP class representing an Intent
+ * filter for the EIP class
+ */
+ private void eipCommand(String action){
+ // TODO validate "action"...how do we get the list of intent-filters for a class via Android API?
+ Intent vpnIntent = new Intent(action);
+ vpnIntent.putExtra(EIP.RECEIVER_TAG, mEIPReceiver);
+ getActivity().startService(vpnIntent);
+ }
+
+ @Override
+ public void updateState(final String state, final String logmessage, final int localizedResId) {
+ // Note: "states" are not organized anywhere...collected state strings:
+ // NOPROCESS,NONETWORK,BYTECOUNT,AUTH_FAILED + some parsing thing ( WAIT(?),AUTH,GET_CONFIG,ASSIGN_IP,CONNECTED,SIGINT )
+ getActivity().runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (eipStatus != null) {
+ boolean switchState = true;
+ String statusMessage = "";
+ String prefix = getString(localizedResId);
+ if (state.equals("CONNECTED")){
+
+ statusMessage = getString(R.string.eip_state_connected);
+ getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+ } else if (state.equals("BYTECOUNT")) {
+ statusMessage = getString(R.string.eip_state_connected); getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+
+ } else if ( (state.equals("NOPROCESS") && !mEipStartPending ) || state.equals("EXITING") && !mEipStartPending || state.equals("FATAL")) {
+ statusMessage = getString(R.string.eip_state_not_connected);
+ getActivity().findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ mEipStartPending = false;
+ switchState = false;
+ } else if (state.equals("NOPROCESS")){
+ statusMessage = logmessage;
+ } else if (state.equals("ASSIGN_IP")){ //don't show assigning message in eipStatus
+ statusMessage = (String) eipStatus.getText();
+ }
+ else {
+ statusMessage = prefix + " " + logmessage;
+ }
+
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(switchState);
+ eipAutoSwitched = false;
+ eipStatus.setText(statusMessage);
+ }
+ }
+ });
+ }
+
+
+ /**
+ * Inner class for handling messages related to EIP status and control requests
+ *
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ */
+ protected class EIPReceiver extends ResultReceiver {
+
+ protected EIPReceiver(Handler handler){
+ super(handler);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ super.onReceiveResult(resultCode, resultData);
+
+ String request = resultData.getString(EIP.REQUEST_TAG);
+ boolean checked = false;
+
+ if (request == EIP.ACTION_IS_EIP_RUNNING) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ break;
+ }
+ } else if (request == EIP.ACTION_START_EIP) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ eipFragment.findViewById(R.id.eipProgress).setVisibility(View.GONE);
+ break;
+ }
+ } else if (request == EIP.ACTION_STOP_EIP) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = false;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = true;
+ break;
+ }
+ } else if (request == EIP.EIP_NOTIFICATION) {
+ switch (resultCode){
+ case Activity.RESULT_OK:
+ checked = true;
+ break;
+ case Activity.RESULT_CANCELED:
+ checked = false;
+ break;
+ }
+ }
+
+ eipAutoSwitched = true;
+ eipSwitch.setChecked(checked);
+ eipAutoSwitched = false;
+ }
+ }
+
+
+ public static EIPReceiver getReceiver() {
+ return mEIPReceiver;
+ }
+
+ public static boolean isEipSwitchChecked() {
+ return eipSwitch.isChecked();
+ }
+
+ public void checkEipSwitch(boolean checked) {
+ eipSwitch.setChecked(checked);
+ onCheckedChanged(eipSwitch, checked);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java b/app/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java
new file mode 100644
index 00000000..885b5105
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.security.KeyStore;
+
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.SingleClientConnManager;
+import android.content.Context;
+
+/**
+ * Implements an HTTP client, enabling LEAP Android app to manage its own runtime keystore or bypass default Android security measures.
+ *
+ * @author rafa
+ *
+ */
+public class LeapHttpClient extends DefaultHttpClient {
+
+ private static LeapHttpClient client;
+
+ /**
+ * If the class scope client is null, it creates one and imports, if existing, the main certificate from Shared Preferences.
+ * @param context
+ * @return the new client.
+ */
+ public static LeapHttpClient getInstance(String cert_string) {
+ if(client == null) {
+ if(cert_string != null) {
+ ConfigHelper.addTrustedCertificate("provider_ca_certificate", cert_string);
+ }
+ }
+ return client;
+ }
+
+ @Override
+ protected ClientConnectionManager createClientConnectionManager() {
+ SchemeRegistry registry = new SchemeRegistry();
+ registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ registry.register(new Scheme("https", newSslSocketFactory(), 443));
+
+ return new SingleClientConnManager(getParams(), registry);
+ }
+
+ /**
+ * Uses keystore from ConfigHelper for the SSLSocketFactory.
+ * @return
+ */
+ private SSLSocketFactory newSslSocketFactory() {
+ try {
+ KeyStore trusted = ConfigHelper.getKeystore();
+ SSLSocketFactory sf = new SSLSocketFactory(trusted);
+
+ return sf;
+ } catch (Exception e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java b/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java
new file mode 100644
index 00000000..a317d95e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java
@@ -0,0 +1,341 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+import org.jboss.security.srp.SRPParameters;
+
+/**
+ * Implements all SRP algorithm logic.
+ *
+ * It's derived from JBoss implementation, with adjustments to make it work with LEAP platform.
+ *
+ * @author parmegv
+ *
+ */
+public class LeapSRPSession {
+
+ private static String token = "";
+
+ final public static String SALT = "salt";
+ final public static String M1 = "M1";
+ final public static String M2 = "M2";
+ final public static String TOKEN = "token";
+ final public static String AUTHORIZATION_HEADER= "Authorization";
+
+ private SRPParameters params;
+ private String username;
+ private String password;
+ private BigInteger N;
+ private byte[] N_bytes;
+ private BigInteger g;
+ private BigInteger x;
+ private BigInteger v;
+ private BigInteger a;
+ private BigInteger A;
+ private byte[] K;
+ private SecureRandom pseudoRng;
+ /** The M1 = H(H(N) xor H(g) | H(U) | s | A | B | K) hash */
+ private MessageDigest clientHash;
+ /** The M2 = H(A | M | K) hash */
+ private MessageDigest serverHash;
+
+ private static int A_LEN;
+
+ /** Creates a new SRP server session object from the username, password
+ verifier,
+ @param username, the user ID
+ @param password, the user clear text password
+ @param params, the SRP parameters for the session
+ */
+ public LeapSRPSession(String username, String password, SRPParameters params)
+ {
+ this(username, password, params, null);
+ }
+
+ /** Creates a new SRP server session object from the username, password
+ verifier,
+ @param username, the user ID
+ @param password, the user clear text password
+ @param params, the SRP parameters for the session
+ @param abytes, the random exponent used in the A public key
+ */
+ public LeapSRPSession(String username, String password, SRPParameters params,
+ byte[] abytes) {
+ this.params = params;
+ this.g = new BigInteger(1, params.g);
+ N_bytes = ConfigHelper.trim(params.N);
+ this.N = new BigInteger(1, N_bytes);
+ this.username = username;
+ this.password = password;
+
+ try {
+ pseudoRng = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+
+ if( abytes != null ) {
+ A_LEN = 8*abytes.length;
+ /* TODO Why did they put this condition?
+ if( 8*abytes.length != A_LEN )
+ throw new IllegalArgumentException("The abytes param must be "
+ +(A_LEN/8)+" in length, abytes.length="+abytes.length);
+ */
+ this.a = new BigInteger(abytes);
+ }
+ else
+ A_LEN = 64;
+
+ serverHash = newDigest();
+ clientHash = newDigest();
+ }
+
+ /**
+ * Calculates the parameter x of the SRP-6a algorithm.
+ * @param username
+ * @param password
+ * @param salt the salt of the user
+ * @return x
+ */
+ public byte[] calculatePasswordHash(String username, String password, byte[] salt)
+ {
+ //password = password.replaceAll("\\\\", "\\\\\\\\");
+ // Calculate x = H(s | H(U | ':' | password))
+ MessageDigest x_digest = newDigest();
+ // Try to convert the username to a byte[] using ISO-8859-1
+ byte[] user = null;
+ byte[] password_bytes = null;
+ byte[] colon = {};
+ String encoding = "ISO-8859-1";
+ try {
+ user = ConfigHelper.trim(username.getBytes(encoding));
+ colon = ConfigHelper.trim(":".getBytes(encoding));
+ password_bytes = ConfigHelper.trim(password.getBytes(encoding));
+ }
+ catch(UnsupportedEncodingException e) {
+ // Use the default platform encoding
+ user = ConfigHelper.trim(username.getBytes());
+ colon = ConfigHelper.trim(":".getBytes());
+ password_bytes = ConfigHelper.trim(password.getBytes());
+ }
+
+ // Build the hash
+ x_digest.update(user);
+ x_digest.update(colon);
+ x_digest.update(password_bytes);
+ byte[] h = x_digest.digest();
+
+ x_digest.reset();
+ x_digest.update(salt);
+ x_digest.update(h);
+ byte[] x_digest_bytes = x_digest.digest();
+
+ return x_digest_bytes;
+ }
+
+ /**
+ * Calculates the parameter V of the SRP-6a algorithm.
+ * @param k_string constant k predefined by the SRP server implementation.
+ * @return the value of V
+ */
+ private BigInteger calculateV(String k_string) {
+ BigInteger k = new BigInteger(k_string, 16);
+ BigInteger v = k.multiply(g.modPow(x, N)); // g^x % N
+ return v;
+ }
+
+ /**
+ * Calculates the trimmed xor from two BigInteger numbers
+ * @param b1 the positive source to build first BigInteger
+ * @param b2 the positive source to build second BigInteger
+ * @param length
+ * @return
+ */
+ public byte[] xor(byte[] b1, byte[] b2)
+ {
+ //TODO Check if length matters in the order, when b2 is smaller than b1 or viceversa
+ byte[] xor_digest = new BigInteger(1, b1).xor(new BigInteger(1, b2)).toByteArray();
+ return ConfigHelper.trim(xor_digest);
+ }
+
+ /**
+ * @returns The exponential residue (parameter A) to be sent to the server.
+ */
+ public byte[] exponential() {
+ byte[] Abytes = null;
+ if(A == null) {
+ /* If the random component of A has not been specified use a random
+ number */
+ if( a == null ) {
+ BigInteger one = BigInteger.ONE;
+ do {
+ a = new BigInteger(A_LEN, pseudoRng);
+ } while(a.compareTo(one) <= 0);
+ }
+ A = g.modPow(a, N);
+ Abytes = ConfigHelper.trim(A.toByteArray());
+ }
+ return Abytes;
+ }
+
+ /**
+ * Calculates the parameter M1, to be sent to the SRP server.
+ * It also updates hashes of client and server for further calculations in other methods.
+ * It uses a predefined k.
+ * @param salt_bytes
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return the parameter M1
+ * @throws NoSuchAlgorithmException
+ */
+ public byte[] response(byte[] salt_bytes, byte[] Bbytes) throws NoSuchAlgorithmException {
+ // Calculate x = H(s | H(U | ':' | password))
+ byte[] M1 = null;
+ if(new BigInteger(1, Bbytes).mod(new BigInteger(1, N_bytes)) != BigInteger.ZERO) {
+ byte[] xb = calculatePasswordHash(username, password, ConfigHelper.trim(salt_bytes));
+ this.x = new BigInteger(1, xb);
+
+ // Calculate v = kg^x mod N
+ String k_string = "bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0";
+ this.v = calculateV(k_string);
+
+ // H(N)
+ byte[] digest_of_n = newDigest().digest(N_bytes);
+
+ // H(g)
+ byte[] digest_of_g = newDigest().digest(params.g);
+
+ // clientHash = H(N) xor H(g)
+ byte[] xor_digest = xor(digest_of_n, digest_of_g);
+ clientHash.update(xor_digest);
+
+ // clientHash = H(N) xor H(g) | H(U)
+ byte[] username_digest = newDigest().digest(ConfigHelper.trim(username.getBytes()));
+ username_digest = ConfigHelper.trim(username_digest);
+ clientHash.update(username_digest);
+
+ // clientHash = H(N) xor H(g) | H(U) | s
+ clientHash.update(ConfigHelper.trim(salt_bytes));
+
+ K = null;
+
+ // clientHash = H(N) xor H(g) | H(U) | A
+ byte[] Abytes = ConfigHelper.trim(A.toByteArray());
+ clientHash.update(Abytes);
+
+ // clientHash = H(N) xor H(g) | H(U) | s | A | B
+ Bbytes = ConfigHelper.trim(Bbytes);
+ clientHash.update(Bbytes);
+
+ // Calculate S = (B - kg^x) ^ (a + u * x) % N
+ BigInteger S = calculateS(Bbytes);
+ byte[] S_bytes = ConfigHelper.trim(S.toByteArray());
+
+ // K = SessionHash(S)
+ String hash_algorithm = params.hashAlgorithm;
+ MessageDigest sessionDigest = MessageDigest.getInstance(hash_algorithm);
+ K = ConfigHelper.trim(sessionDigest.digest(S_bytes));
+
+ // clientHash = H(N) xor H(g) | H(U) | A | B | K
+ clientHash.update(K);
+
+ M1 = ConfigHelper.trim(clientHash.digest());
+
+ // serverHash = Astr + M + K
+ serverHash.update(Abytes);
+ serverHash.update(M1);
+ serverHash.update(K);
+
+ }
+ return M1;
+ }
+
+ /**
+ * It calculates the parameter S used by response() to obtain session hash K.
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return the parameter S
+ */
+ private BigInteger calculateS(byte[] Bbytes) {
+ byte[] Abytes = ConfigHelper.trim(A.toByteArray());
+ Bbytes = ConfigHelper.trim(Bbytes);
+ byte[] u_bytes = getU(Abytes, Bbytes);
+
+ BigInteger B = new BigInteger(1, Bbytes);
+ BigInteger u = new BigInteger(1, u_bytes);
+
+ BigInteger B_minus_v = B.subtract(v);
+ BigInteger a_ux = a.add(u.multiply(x));
+ BigInteger S = B_minus_v.modPow(a_ux, N);
+ return S;
+ }
+
+ /**
+ * It calculates the parameter u used by calculateS to obtain S.
+ * @param Abytes the exponential residue sent to the server
+ * @param Bbytes the parameter received from the server, in bytes
+ * @return
+ */
+ public byte[] getU(byte[] Abytes, byte[] Bbytes) {
+ MessageDigest u_digest = newDigest();
+ u_digest.update(ConfigHelper.trim(Abytes));
+ u_digest.update(ConfigHelper.trim(Bbytes));
+ byte[] u_digest_bytes = u_digest.digest();
+ return ConfigHelper.trim(new BigInteger(1, u_digest_bytes).toByteArray());
+ }
+
+ /**
+ * @param M2 The server's response to the client's challenge
+ * @returns True if and only if the server's response was correct.
+ */
+ public boolean verify(byte[] M2)
+ {
+ // M2 = H(A | M1 | K)
+ M2 = ConfigHelper.trim(M2);
+ byte[] myM2 = ConfigHelper.trim(serverHash.digest());
+ boolean valid = Arrays.equals(M2, myM2);
+ return valid;
+ }
+
+ protected static void setToken(String token) {
+ LeapSRPSession.token = token;
+ }
+
+ protected static String getToken() {
+ return token;
+ }
+
+ /**
+ * @return a new SHA-256 digest.
+ */
+ public MessageDigest newDigest()
+ {
+ MessageDigest md = null;
+ try {
+ md = MessageDigest.getInstance("SHA-256");
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return md;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/LogInDialog.java b/app/src/main/java/se/leap/bitmaskclient/LogInDialog.java
new file mode 100644
index 00000000..a28c9049
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/LogInDialog.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import android.R.color;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.res.ColorStateList;
+import android.os.Bundle;
+import android.provider.CalendarContract.Colors;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.BounceInterpolator;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Implements the log in dialog, currently without progress dialog.
+ *
+ * It returns to the previous fragment when finished, and sends username and password to the authenticate method.
+ *
+ * It also notifies the user if the password is not valid.
+ *
+ * @author parmegv
+ *
+ */
+public class LogInDialog extends DialogFragment {
+
+
+ final public static String TAG = "logInDialog";
+ final public static String VERB = "log in";
+ final public static String USERNAME = "username";
+ final public static String PASSWORD = "password";
+ final public static String USERNAME_MISSING = "username missing";
+ final public static String PASSWORD_INVALID_LENGTH = "password_invalid_length";
+
+ private static boolean is_eip_pending = false;
+
+ public AlertDialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View log_in_dialog_view = inflater.inflate(R.layout.log_in_dialog, null);
+
+ final TextView user_message = (TextView)log_in_dialog_view.findViewById(R.id.user_message);
+ if(getArguments() != null && getArguments().containsKey(getResources().getString(R.string.user_message))) {
+ user_message.setText(getArguments().getString(getResources().getString(R.string.user_message)));
+ } else {
+ user_message.setVisibility(View.GONE);
+ }
+
+ final EditText username_field = (EditText)log_in_dialog_view.findViewById(R.id.username_entered);
+ if(getArguments() != null && getArguments().containsKey(USERNAME)) {
+ String username = getArguments().getString(USERNAME);
+ username_field.setText(username);
+ }
+ if (getArguments() != null && getArguments().containsKey(USERNAME_MISSING)) {
+ username_field.setError(getResources().getString(R.string.username_ask));
+ }
+
+ final EditText password_field = (EditText)log_in_dialog_view.findViewById(R.id.password_entered);
+ if(!username_field.getText().toString().isEmpty() && password_field.isFocusable()) {
+ password_field.requestFocus();
+ }
+ if (getArguments() != null && getArguments().containsKey(PASSWORD_INVALID_LENGTH)) {
+ password_field.setError(getResources().getString(R.string.error_not_valid_password_user_message));
+ }
+ if(getArguments() != null && getArguments().getBoolean(EipServiceFragment.IS_EIP_PENDING, false)) {
+ is_eip_pending = true;
+ }
+
+
+ builder.setView(log_in_dialog_view)
+ .setPositiveButton(R.string.login_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String username = username_field.getText().toString();
+ String password = password_field.getText().toString();
+ dialog.dismiss();
+ interface_with_Dashboard.authenticate(username, password);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+
+ return builder.create();
+ }
+
+ /**
+ * Interface used to communicate LogInDialog with Dashboard.
+ *
+ * @author parmegv
+ *
+ */
+ public interface LogInDialogInterface {
+ /**
+ * Starts authentication process.
+ * @param username
+ * @param password
+ */
+ public void authenticate(String username, String password);
+ public void cancelAuthedEipOn();
+ }
+
+ LogInDialogInterface interface_with_Dashboard;
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance() {
+ LogInDialog dialog_fragment = new LogInDialog();
+ return dialog_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_Dashboard = (LogInDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement LogInDialogListener");
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if(is_eip_pending)
+ interface_with_Dashboard.cancelAuthedEipOn();
+ super.onCancel(dialog);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java b/app/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java
new file mode 100644
index 00000000..cf09c64b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/NewProviderDialog.java
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import se.leap.bitmaskclient.R;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * Implements the new custom provider dialog.
+ *
+ * @author parmegv
+ *
+ */
+public class NewProviderDialog extends DialogFragment {
+
+ final public static String TAG = "newProviderDialog";
+
+ public interface NewProviderDialogInterface {
+ public void showAndSelectProvider(String url_provider, boolean danger_on);
+ }
+
+ NewProviderDialogInterface interface_with_ConfigurationWizard;
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance() {
+ NewProviderDialog dialog_fragment = new NewProviderDialog();
+ return dialog_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_ConfigurationWizard = (NewProviderDialogInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View new_provider_dialog_view = inflater.inflate(R.layout.new_provider_dialog, null);
+ final EditText url_input_field = (EditText)new_provider_dialog_view.findViewById(R.id.new_provider_url);
+ if(getArguments() != null && getArguments().containsKey(Provider.MAIN_URL)) {
+ url_input_field.setText(getArguments().getString(Provider.MAIN_URL));
+ }
+ final CheckBox danger_checkbox = (CheckBox)new_provider_dialog_view.findViewById(R.id.danger_checkbox);
+ if(getArguments() != null && getArguments().containsKey(ProviderItem.DANGER_ON)) {
+ danger_checkbox.setActivated(getArguments().getBoolean(ProviderItem.DANGER_ON));
+ }
+
+ builder.setView(new_provider_dialog_view)
+ .setMessage(R.string.introduce_new_provider)
+ .setPositiveButton(R.string.save, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ String entered_url = url_input_field.getText().toString().trim();
+ if(!entered_url.startsWith("https://")) {
+ if (entered_url.startsWith("http://")){
+ entered_url = entered_url.substring("http://".length());
+ }
+ entered_url = "https://".concat(entered_url);
+ }
+ boolean danger_on = danger_checkbox.isChecked();
+ if(validURL(entered_url)) {
+ interface_with_ConfigurationWizard.showAndSelectProvider(entered_url, danger_on);
+ Toast.makeText(getActivity().getApplicationContext(), R.string.valid_url_entered, Toast.LENGTH_LONG).show();
+ } else {
+ url_input_field.setText("");
+ danger_checkbox.setChecked(false);
+ Toast.makeText(getActivity().getApplicationContext(), R.string.not_valid_url_entered, Toast.LENGTH_LONG).show();;
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+ /**
+ * Checks if the entered url is valid or not.
+ * @param entered_url
+ * @return true if it's not empty nor contains only the protocol.
+ */
+ boolean validURL(String entered_url) {
+ //return !entered_url.isEmpty() && entered_url.matches("http[s]?://.+") && !entered_url.replaceFirst("http[s]?://", "").isEmpty();
+ return android.util.Patterns.WEB_URL.matcher(entered_url).matches();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java b/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java
new file mode 100644
index 00000000..a046f01f
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java
@@ -0,0 +1,338 @@
+package se.leap.bitmaskclient;
+
+/*
+ * This software is provided 'as-is', without any express or implied
+ * warranty. In no event will Google be held liable for any damages
+ * arising from the use of this software.
+ *
+ * Permission is granted to anyone to use this software for any purpose,
+ * including commercial applications, and to alter it and redistribute it
+ * freely, as long as the origin is not misrepresented.
+ *
+ * Source: http://android-developers.blogspot.de/2013/08/some-securerandom-thoughts.html
+ */
+
+import android.os.Build;
+import android.os.Process;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
+import java.security.SecureRandom;
+import java.security.SecureRandomSpi;
+import java.security.Security;
+
+/**
+ * Fixes for the output of the default PRNG having low entropy.
+ *
+ * The fixes need to be applied via {@link #apply()} before any use of Java
+ * Cryptography Architecture primitives. A good place to invoke them is in the
+ * application's {@code onCreate}.
+ */
+public final class PRNGFixes {
+
+ private static final int VERSION_CODE_JELLY_BEAN = 16;
+ private static final int VERSION_CODE_JELLY_BEAN_MR2 = 18;
+ private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL =
+ getBuildFingerprintAndDeviceSerial();
+
+ /** Hidden constructor to prevent instantiation. */
+ private PRNGFixes() {}
+
+ /**
+ * Applies all fixes.
+ *
+ * @throws SecurityException if a fix is needed but could not be applied.
+ */
+ public static void apply() {
+ applyOpenSSLFix();
+ installLinuxPRNGSecureRandom();
+ }
+
+ /**
+ * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the
+ * fix is not needed.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void applyOpenSSLFix() throws SecurityException {
+ if ((Build.VERSION.SDK_INT < VERSION_CODE_JELLY_BEAN)
+ || (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2)) {
+ // No need to apply the fix
+ return;
+ }
+
+ try {
+ // Mix in the device- and invocation-specific seed.
+ Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_seed", byte[].class)
+ .invoke(null, generateSeed());
+
+ // Mix output of Linux PRNG into OpenSSL's PRNG
+ int bytesRead = (Integer) Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.NativeCrypto")
+ .getMethod("RAND_load_file", String.class, long.class)
+ .invoke(null, "/dev/urandom", 1024);
+ if (bytesRead != 1024) {
+ throw new IOException(
+ "Unexpected number of bytes read from Linux PRNG: "
+ + bytesRead);
+ }
+ } catch (Exception e) {
+ throw new SecurityException("Failed to seed OpenSSL PRNG", e);
+ }
+ }
+
+ /**
+ * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the
+ * default. Does nothing if the implementation is already the default or if
+ * there is not need to install the implementation.
+ *
+ * @throws SecurityException if the fix is needed but could not be applied.
+ */
+ private static void installLinuxPRNGSecureRandom()
+ throws SecurityException {
+ if (Build.VERSION.SDK_INT > VERSION_CODE_JELLY_BEAN_MR2) {
+ // No need to apply the fix
+ return;
+ }
+
+ // Install a Linux PRNG-based SecureRandom implementation as the
+ // default, if not yet installed.
+ Provider[] secureRandomProviders =
+ Security.getProviders("SecureRandom.SHA1PRNG");
+ if ((secureRandomProviders == null)
+ || (secureRandomProviders.length < 1)
+ || (!LinuxPRNGSecureRandomProvider.class.equals(
+ secureRandomProviders[0].getClass()))) {
+ Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1);
+ }
+
+ // Assert that new SecureRandom() and
+ // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed
+ // by the Linux PRNG-based SecureRandom implementation.
+ SecureRandom rng1 = new SecureRandom();
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng1.getProvider().getClass())) {
+ throw new SecurityException(
+ "new SecureRandom() backed by wrong Provider: "
+ + rng1.getProvider().getClass());
+ }
+
+ SecureRandom rng2;
+ try {
+ rng2 = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new SecurityException("SHA1PRNG not available", e);
+ }
+ if (!LinuxPRNGSecureRandomProvider.class.equals(
+ rng2.getProvider().getClass())) {
+ throw new SecurityException(
+ "SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong"
+ + " Provider: " + rng2.getProvider().getClass());
+ }
+ }
+
+ /**
+ * {@code Provider} of {@code SecureRandom} engines which pass through
+ * all requests to the Linux PRNG.
+ */
+ private static class LinuxPRNGSecureRandomProvider extends Provider {
+
+ public LinuxPRNGSecureRandomProvider() {
+ super("LinuxPRNG",
+ 1.0,
+ "A Linux-specific random number provider that uses"
+ + " /dev/urandom");
+ // Although /dev/urandom is not a SHA-1 PRNG, some apps
+ // explicitly request a SHA1PRNG SecureRandom and we thus need to
+ // prevent them from getting the default implementation whose output
+ // may have low entropy.
+ put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName());
+ put("SecureRandom.SHA1PRNG ImplementedIn", "Software");
+ }
+ }
+
+ /**
+ * {@link SecureRandomSpi} which passes all requests to the Linux PRNG
+ * ({@code /dev/urandom}).
+ */
+ public static class LinuxPRNGSecureRandom extends SecureRandomSpi {
+
+ /*
+ * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed
+ * are passed through to the Linux PRNG (/dev/urandom). Instances of
+ * this class seed themselves by mixing in the current time, PID, UID,
+ * build fingerprint, and hardware serial number (where available) into
+ * Linux PRNG.
+ *
+ * Concurrency: Read requests to the underlying Linux PRNG are
+ * serialized (on sLock) to ensure that multiple threads do not get
+ * duplicated PRNG output.
+ */
+
+ private static final File URANDOM_FILE = new File("/dev/urandom");
+
+ private static final Object sLock = new Object();
+
+ /**
+ * Input stream for reading from Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static DataInputStream sUrandomIn;
+
+ /**
+ * Output stream for writing to Linux PRNG or {@code null} if not yet
+ * opened.
+ *
+ * @GuardedBy("sLock")
+ */
+ private static OutputStream sUrandomOut;
+
+ /**
+ * Whether this engine instance has been seeded. This is needed because
+ * each instance needs to seed itself if the client does not explicitly
+ * seed it.
+ */
+ private boolean mSeeded;
+
+ @Override
+ protected void engineSetSeed(byte[] bytes) {
+ try {
+ OutputStream out;
+ synchronized (sLock) {
+ out = getUrandomOutputStream();
+ }
+ out.write(bytes);
+ out.flush();
+ } catch (IOException e) {
+ // On a small fraction of devices /dev/urandom is not writable.
+ // Log and ignore.
+ Log.w(PRNGFixes.class.getSimpleName(),
+ "Failed to mix seed into " + URANDOM_FILE);
+ } finally {
+ mSeeded = true;
+ }
+ }
+
+ @Override
+ protected void engineNextBytes(byte[] bytes) {
+ if (!mSeeded) {
+ // Mix in the device- and invocation-specific seed.
+ engineSetSeed(generateSeed());
+ }
+
+ try {
+ DataInputStream in;
+ synchronized (sLock) {
+ in = getUrandomInputStream();
+ }
+ synchronized (in) {
+ in.readFully(bytes);
+ }
+ } catch (IOException e) {
+ throw new SecurityException(
+ "Failed to read from " + URANDOM_FILE, e);
+ }
+ }
+
+ @Override
+ protected byte[] engineGenerateSeed(int size) {
+ byte[] seed = new byte[size];
+ engineNextBytes(seed);
+ return seed;
+ }
+
+ private DataInputStream getUrandomInputStream() {
+ synchronized (sLock) {
+ if (sUrandomIn == null) {
+ // NOTE: Consider inserting a BufferedInputStream between
+ // DataInputStream and FileInputStream if you need higher
+ // PRNG output performance and can live with future PRNG
+ // output being pulled into this process prematurely.
+ try {
+ sUrandomIn = new DataInputStream(
+ new FileInputStream(URANDOM_FILE));
+ } catch (IOException e) {
+ throw new SecurityException("Failed to open "
+ + URANDOM_FILE + " for reading", e);
+ }
+ }
+ return sUrandomIn;
+ }
+ }
+
+ private OutputStream getUrandomOutputStream() throws IOException {
+ synchronized (sLock) {
+ if (sUrandomOut == null) {
+ sUrandomOut = new FileOutputStream(URANDOM_FILE);
+ }
+ return sUrandomOut;
+ }
+ }
+ }
+
+ /**
+ * Generates a device- and invocation-specific seed to be mixed into the
+ * Linux PRNG.
+ */
+ private static byte[] generateSeed() {
+ try {
+ ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream();
+ DataOutputStream seedBufferOut =
+ new DataOutputStream(seedBuffer);
+ seedBufferOut.writeLong(System.currentTimeMillis());
+ seedBufferOut.writeLong(System.nanoTime());
+ seedBufferOut.writeInt(Process.myPid());
+ seedBufferOut.writeInt(Process.myUid());
+ seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL);
+ seedBufferOut.close();
+ return seedBuffer.toByteArray();
+ } catch (IOException e) {
+ throw new SecurityException("Failed to generate seed", e);
+ }
+ }
+
+ /**
+ * Gets the hardware serial number of this device.
+ *
+ * @return serial number or {@code null} if not available.
+ */
+ private static String getDeviceSerialNumber() {
+ // We're using the Reflection API because Build.SERIAL is only available
+ // since API Level 9 (Gingerbread, Android 2.3).
+ try {
+ return (String) Build.class.getField("SERIAL").get(null);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private static byte[] getBuildFingerprintAndDeviceSerial() {
+ StringBuilder result = new StringBuilder();
+ String fingerprint = Build.FINGERPRINT;
+ if (fingerprint != null) {
+ result.append(fingerprint);
+ }
+ String serial = getDeviceSerialNumber();
+ if (serial != null) {
+ result.append(serial);
+ }
+ try {
+ return result.toString().getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException("UTF-8 encoding not supported");
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
new file mode 100644
index 00000000..216f4261
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Locale;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.app.Activity;
+import android.content.SharedPreferences;
+
+/**
+ * @author Sean Leonard <meanderingcode@aetherislands.net>
+ *
+ */
+public final class Provider implements Serializable {
+
+ private static final long serialVersionUID = 6003835972151761353L;
+
+ private static Provider instance = null;
+
+ // We'll access our preferences here
+ private static SharedPreferences preferences = null;
+ // Represents our Provider's provider.json
+ private static JSONObject definition = null;
+
+ final public static String
+ API_URL = "api_uri",
+ API_VERSION = "api_version",
+ ALLOW_REGISTRATION = "allow_registration",
+ API_RETURN_SERIAL = "serial",
+ SERVICE = "service",
+ KEY = "provider",
+ CA_CERT = "ca_cert",
+ NAME = "name",
+ DESCRIPTION = "description",
+ DOMAIN = "domain",
+ MAIN_URL = "main_url",
+ DOT_JSON_URL = "provider_json_url"
+ ;
+
+ // Array of what API versions we understand
+ protected static final String[] API_VERSIONS = {"1"}; // I assume we might encounter arbitrary version "numbers"
+ // Some API pieces we want to know about
+ private static final String API_TERM_SERVICES = "services";
+ private static final String API_TERM_NAME = "name";
+ private static final String API_TERM_DOMAIN = "domain";
+ private static final String API_TERM_DEFAULT_LANGUAGE = "default_language";
+ protected static final String[] API_EIP_TYPES = {"openvpn"};
+
+ private static final String PREFS_EIP_NAME = null;
+
+
+
+ // What, no individual fields?! We're going to gamble on org.json.JSONObject and JSONArray
+ // Supporting multiple API versions will probably break this paradigm,
+ // Forcing me to write a real constructor and rewrite getters/setters
+ // Also will refactor if i'm instantiating the same local variables all the time
+
+ /**
+ *
+ */
+ private Provider() {}
+
+ protected static Provider getInstance(){
+ if(instance==null){
+ instance = new Provider();
+ }
+ return instance;
+ }
+
+ protected void init(Activity activity) {
+
+ // Load our preferences from SharedPreferences
+ // If there's nothing there, we will end up returning a rather empty object
+ // to whoever called getInstance() and they can run the First Run Wizard
+ //preferences = context.getgetPreferences(0); // 0 == MODE_PRIVATE, but we don't extend Android's classes...
+
+ // Load SharedPreferences
+ preferences = activity.getSharedPreferences(Dashboard.SHARED_PREFERENCES,Context.MODE_PRIVATE);
+ // Inflate our provider.json data
+ try {
+ definition = new JSONObject( preferences.getString(Provider.KEY, "") );
+ } catch (JSONException e) {
+ // TODO: handle exception
+
+ // FIXME!! We want "real" data!!
+ }
+ }
+
+ protected String getDomain(){
+ String domain = "Null";
+ try {
+ domain = definition.getString(API_TERM_DOMAIN);
+ } catch (JSONException e) {
+ domain = "Null";
+ e.printStackTrace();
+ }
+ return domain;
+ }
+
+ protected String getName(){
+ // Should we pass the locale in, or query the system here?
+ String lang = Locale.getDefault().getLanguage();
+ String name = "Null"; // Should it actually /be/ null, for error conditions?
+ try {
+ name = definition.getJSONObject(API_TERM_NAME).getString(lang);
+ } catch (JSONException e) {
+ // TODO: Nesting try/catch blocks? Crazy
+ // Maybe you should actually handle exception?
+ try {
+ name = definition.getJSONObject(API_TERM_NAME).getString( definition.getString(API_TERM_DEFAULT_LANGUAGE) );
+ } catch (JSONException e2) {
+ // TODO: Will you handle the exception already?
+ }
+ }
+
+ return name;
+ }
+
+ protected String getDescription(){
+ String lang = Locale.getDefault().getLanguage();
+ String desc = null;
+ try {
+ desc = definition.getJSONObject("description").getString(lang);
+ } catch (JSONException e) {
+ // TODO: handle exception!!
+ try {
+ desc = definition.getJSONObject("description").getString( definition.getString("default_language") );
+ } catch (JSONException e2) {
+ // TODO: i can't believe you're doing it again!
+ }
+ }
+
+ return desc;
+ }
+
+ protected boolean hasEIP() {
+ JSONArray services = null;
+ try {
+ services = definition.getJSONArray(API_TERM_SERVICES); // returns ["openvpn"]
+ } catch (Exception e) {
+ // TODO: handle exception
+ }
+ for (int i=0;i<API_EIP_TYPES.length+1;i++){
+ try {
+ // Walk the EIP types array looking for matches in provider's service definitions
+ if ( Arrays.asList(API_EIP_TYPES).contains( services.getString(i) ) )
+ return true;
+ } catch (NullPointerException e){
+ e.printStackTrace();
+ return false;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }
+ }
+ return false;
+ }
+
+ protected String getEIPType() {
+ // FIXME!!!!! We won't always be providing /only/ OpenVPN, will we?
+ // This will have to hook into some saved choice of EIP transport
+ if ( instance.hasEIP() )
+ return "OpenVPN";
+ else
+ return null;
+ }
+
+ protected JSONObject getEIP() {
+ // FIXME!!!!! We won't always be providing /only/ OpenVPN, will we?
+ // This will have to hook into some saved choice of EIP transport, cluster, gateway
+ // with possible "choose at random" preference
+ if ( instance.hasEIP() ){
+ // TODO Might need an EIP class, but we've only got OpenVPN type right now,
+ // and only one gateway for our only provider...
+ // TODO We'll try to load from preferences, have to call ProviderAPI if we've got nothin...
+ JSONObject eipObject = null;
+ try {
+ eipObject = new JSONObject( preferences.getString(PREFS_EIP_NAME, "") );
+ } catch (JSONException e) {
+ // TODO ConfigHelper.rescueJSON()
+ // Still nothing?
+ // TODO ProviderAPI.getEIP()
+ e.printStackTrace();
+ }
+
+ return eipObject;
+ } else
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
new file mode 100644
index 00000000..75ef511d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -0,0 +1,875 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.DataOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Scanner;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import org.apache.http.client.ClientProtocolException;
+import org.jboss.security.srp.SRPParameters;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+import android.util.Base64;
+import android.util.Log;
+
+
+/**
+ * Implements HTTP api methods used to manage communications with the provider server.
+ *
+ * It's an IntentService because it downloads data from the Internet, so it operates in the background.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ *
+ */
+public class ProviderAPI extends IntentService {
+
+ private Handler mHandler;
+
+ final public static String
+ SET_UP_PROVIDER = "setUpProvider",
+ DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
+ SRP_REGISTER = "srpRegister",
+ SRP_AUTH = "srpAuth",
+ LOG_OUT = "logOut",
+ DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
+ PARAMETERS = "parameters",
+ RESULT_KEY = "result",
+ RECEIVER_KEY = "receiver",
+ SESSION_ID_COOKIE_KEY = "session_id_cookie_key",
+ SESSION_ID_KEY = "session_id",
+ ERRORS = "errors",
+ UPDATE_PROGRESSBAR = "update_progressbar",
+ CURRENT_PROGRESS = "current_progress",
+ TAG = "provider_api_tag"
+ ;
+
+ final public static int
+ CUSTOM_PROVIDER_ADDED = 0,
+ SRP_AUTHENTICATION_SUCCESSFUL = 3,
+ SRP_AUTHENTICATION_FAILED = 4,
+ SRP_REGISTRATION_SUCCESSFUL = 5,
+ SRP_REGISTRATION_FAILED = 6,
+ LOGOUT_SUCCESSFUL = 7,
+ LOGOUT_FAILED = 8,
+ CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
+ INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
+ PROVIDER_OK = 11,
+ PROVIDER_NOK = 12,
+ CORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 13,
+ INCORRECTLY_DOWNLOADED_ANON_CERTIFICATE = 14
+ ;
+
+ private static boolean
+ CA_CERT_DOWNLOADED = false,
+ PROVIDER_JSON_DOWNLOADED = false,
+ EIP_SERVICE_JSON_DOWNLOADED = false
+ ;
+
+ private static String last_provider_main_url;
+ private static boolean last_danger_on = false;
+ private static boolean setting_up_provider = true;
+
+ public static void stop() {
+ setting_up_provider = false;
+ }
+
+ public ProviderAPI() {
+ super("ProviderAPI");
+ Log.v("ClassName", "Provider API");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mHandler = new Handler();
+ CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER) );
+ }
+
+ public static String lastProviderMainUrl() {
+ return last_provider_main_url;
+ }
+
+ public static boolean lastDangerOn() {
+ return last_danger_on;
+ }
+
+ private String formatErrorMessage(final int toast_string_id) {
+ return "{ \"" + ERRORS + "\" : \""+getResources().getString(toast_string_id)+"\" }";
+ }
+
+ @Override
+ protected void onHandleIntent(Intent command) {
+ final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY);
+ String action = command.getAction();
+ Bundle parameters = command.getBundleExtra(PARAMETERS);
+ setting_up_provider = true;
+
+ if(action.equalsIgnoreCase(SET_UP_PROVIDER)) {
+ Bundle result = setUpProvider(parameters);
+ if(setting_up_provider) {
+ if(result.getBoolean(RESULT_KEY)) {
+ receiver.send(PROVIDER_OK, result);
+ } else {
+ receiver.send(PROVIDER_NOK, result);
+ }
+ }
+ } else if (action.equalsIgnoreCase(SRP_AUTH)) {
+ Bundle session_id_bundle = authenticateBySRP(parameters);
+ if(session_id_bundle.getBoolean(RESULT_KEY)) {
+ receiver.send(SRP_AUTHENTICATION_SUCCESSFUL, session_id_bundle);
+ } else {
+ receiver.send(SRP_AUTHENTICATION_FAILED, session_id_bundle);
+ }
+ } else if (action.equalsIgnoreCase(LOG_OUT)) {
+ if(logOut(parameters)) {
+ receiver.send(LOGOUT_SUCCESSFUL, Bundle.EMPTY);
+ } else {
+ receiver.send(LOGOUT_FAILED, Bundle.EMPTY);
+ }
+ } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) {
+ if(getNewCert(parameters)) {
+ receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY);
+ } else {
+ receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY);
+ }
+ }
+ }
+
+ /**
+ * Starts the authentication process using SRP protocol.
+ *
+ * @param task containing: username, password and api url.
+ * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful.
+ */
+ private Bundle authenticateBySRP(Bundle task) {
+ Bundle session_id_bundle = new Bundle();
+ int progress = 0;
+
+ String username = (String) task.get(LogInDialog.USERNAME);
+ String password = (String) task.get(LogInDialog.PASSWORD);
+ if(validUserLoginData(username, password)) {
+
+ String authentication_server = (String) task.get(Provider.API_URL);
+
+ SRPParameters params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256");
+ LeapSRPSession client = new LeapSRPSession(username, password, params);
+ byte[] A = client.exponential();
+ broadcast_progress(progress++);
+ try {
+ JSONObject saltAndB = sendAToSRPServer(authentication_server, username, new BigInteger(1, A).toString(16));
+ if(saltAndB.length() > 0) {
+ String salt = saltAndB.getString(LeapSRPSession.SALT);
+ broadcast_progress(progress++);
+ byte[] Bbytes = new BigInteger(saltAndB.getString("B"), 16).toByteArray();
+ byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
+ if(M1 != null) {
+ broadcast_progress(progress++);
+ JSONObject session_idAndM2 = sendM1ToSRPServer(authentication_server, username, M1);
+ if(session_idAndM2.has(LeapSRPSession.M2) && client.verify((byte[])session_idAndM2.get(LeapSRPSession.M2))) {
+ session_id_bundle.putBoolean(RESULT_KEY, true);
+ broadcast_progress(progress++);
+ } else {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ }
+ } else {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message));
+ }
+ broadcast_progress(progress++);
+ } else {
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_bad_user_password_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ }
+ } catch (ClientProtocolException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_client_http_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (IOException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_io_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (JSONException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_json_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (NoSuchAlgorithmException e) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_no_such_algorithm_exception_user_message));
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ } else {
+ if(!wellFormedPassword(password)) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putString(LogInDialog.USERNAME, username);
+ session_id_bundle.putBoolean(LogInDialog.PASSWORD_INVALID_LENGTH, true);
+ }
+ if(username.isEmpty()) {
+ session_id_bundle.putBoolean(RESULT_KEY, false);
+ session_id_bundle.putBoolean(LogInDialog.USERNAME_MISSING, true);
+ }
+ }
+
+ return session_id_bundle;
+ }
+
+ /**
+ * Sets up an intent with the progress value passed as a parameter
+ * and sends it as a broadcast.
+ * @param progress
+ */
+ private void broadcast_progress(int progress) {
+ Intent intentUpdate = new Intent();
+ intentUpdate.setAction(UPDATE_PROGRESSBAR);
+ intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
+ intentUpdate.putExtra(CURRENT_PROGRESS, progress);
+ sendBroadcast(intentUpdate);
+ }
+
+ /**
+ * Validates parameters entered by the user to log in
+ * @param entered_username
+ * @param entered_password
+ * @return true if both parameters are present and the entered password length is greater or equal to eight (8).
+ */
+ private boolean validUserLoginData(String entered_username, String entered_password) {
+ return !(entered_username.isEmpty()) && wellFormedPassword(entered_password);
+ }
+
+ /**
+ * Validates a password
+ * @param entered_password
+ * @return true if the entered password length is greater or equal to eight (8).
+ */
+ private boolean wellFormedPassword(String entered_password) {
+ return entered_password.length() >= 8;
+ }
+
+ /**
+ * Sends an HTTP POST request to the authentication server with the SRP Parameter A.
+ * @param server_url
+ * @param username
+ * @param clientA First SRP parameter sent
+ * @return response from authentication server
+ * @throws ClientProtocolException
+ * @throws IOException
+ * @throws JSONException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendAToSRPServer(String server_url, String username, String clientA) throws ClientProtocolException, IOException, JSONException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put("login", username);
+ parameters.put("A", clientA);
+ return sendToServer(server_url + "/sessions.json", "POST", parameters);
+
+ /*HttpPost post = new HttpPost(server_url + "/sessions.json" + "?" + "login=" + username + "&&" + "A=" + clientA);
+ return sendToServer(post);*/
+ }
+
+ /**
+ * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M).
+ * @param server_url
+ * @param username
+ * @param m1 Second SRP parameter sent
+ * @return response from authentication server
+ * @throws ClientProtocolException
+ * @throws IOException
+ * @throws JSONException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1) throws ClientProtocolException, IOException, JSONException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ Map<String, String> parameters = new HashMap<String, String>();
+ parameters.put("client_auth", new BigInteger(1, ConfigHelper.trim(m1)).toString(16));
+
+ //HttpPut put = new HttpPut(server_url + "/sessions/" + username +".json" + "?" + "client_auth" + "=" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16));
+ JSONObject json_response = sendToServer(server_url + "/sessions/" + username +".json", "PUT", parameters);
+
+ JSONObject session_idAndM2 = new JSONObject();
+ if(json_response.length() > 0) {
+ byte[] M2_not_trimmed = new BigInteger(json_response.getString(LeapSRPSession.M2), 16).toByteArray();
+ /*Cookie session_id_cookie = LeapHttpClient.getInstance(getApplicationContext()).getCookieStore().getCookies().get(0);
+ session_idAndM2.put(ConfigHelper.SESSION_ID_COOKIE_KEY, session_id_cookie.getName());
+ session_idAndM2.put(ConfigHelper.SESSION_ID_KEY, session_id_cookie.getValue());*/
+ session_idAndM2.put(LeapSRPSession.M2, ConfigHelper.trim(M2_not_trimmed));
+ CookieHandler.setDefault(null); // we don't need cookies anymore
+ String token = json_response.getString(LeapSRPSession.TOKEN);
+ LeapSRPSession.setToken(token);
+ }
+ return session_idAndM2;
+ }
+
+ /**
+ * Executes an HTTP request expecting a JSON response.
+ * @param url
+ * @param request_method
+ * @param parameters
+ * @return response from authentication server
+ * @throws IOException
+ * @throws JSONException
+ * @throws MalformedURLException
+ * @throws CertificateException
+ * @throws NoSuchAlgorithmException
+ * @throws KeyStoreException
+ * @throws KeyManagementException
+ */
+ private JSONObject sendToServer(String url, String request_method, Map<String, String> parameters) throws JSONException, MalformedURLException, IOException, KeyManagementException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
+ JSONObject json_response;
+ InputStream is = null;
+ HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(url).openConnection();
+ urlConnection.setRequestMethod(request_method);
+ urlConnection.setChunkedStreamingMode(0);
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+ try {
+
+ DataOutputStream writer = new DataOutputStream(urlConnection.getOutputStream());
+ writer.writeBytes(formatHttpParameters(parameters));
+ writer.close();
+
+ is = urlConnection.getInputStream();
+ String plain_response = new Scanner(is).useDelimiter("\\A").next();
+ json_response = new JSONObject(plain_response);
+ } finally {
+ InputStream error_stream = urlConnection.getErrorStream();
+ if(error_stream != null) {
+ String error_response = new Scanner(error_stream).useDelimiter("\\A").next();
+ urlConnection.disconnect();
+ Log.d("Error", error_response);
+ json_response = new JSONObject(error_response);
+ if(!json_response.isNull(ERRORS) || json_response.has(ERRORS)) {
+ return new JSONObject();
+ }
+ }
+ }
+
+ return json_response;
+ }
+
+ private String formatHttpParameters(Map<String, String> parameters) throws UnsupportedEncodingException {
+ StringBuilder result = new StringBuilder();
+ boolean first = true;
+
+ Iterator<String> parameter_iterator = parameters.keySet().iterator();
+ while(parameter_iterator.hasNext()) {
+ if(first)
+ first = false;
+ else
+ result.append("&&");
+
+ String key = parameter_iterator.next();
+ String value = parameters.get(key);
+
+ result.append(URLEncoder.encode(key, "UTF-8"));
+ result.append("=");
+ result.append(URLEncoder.encode(value, "UTF-8"));
+ }
+
+ return result.toString();
+ }
+
+
+
+
+ /**
+ * Downloads a provider.json from a given URL, adding a new provider using the given name.
+ * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url.
+ * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful.
+ */
+ private Bundle setUpProvider(Bundle task) {
+ int progress = 0;
+ Bundle current_download = new Bundle();
+
+ if(task != null && task.containsKey(ProviderItem.DANGER_ON) && task.containsKey(Provider.MAIN_URL)) {
+ last_danger_on = task.getBoolean(ProviderItem.DANGER_ON);
+ last_provider_main_url = task.getString(Provider.MAIN_URL);
+ CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = EIP_SERVICE_JSON_DOWNLOADED = false;
+ }
+
+ if(!CA_CERT_DOWNLOADED)
+ current_download = downloadCACert(last_provider_main_url, last_danger_on);
+ if(CA_CERT_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+ broadcast_progress(progress++);
+ CA_CERT_DOWNLOADED = true;
+ if(!PROVIDER_JSON_DOWNLOADED)
+ current_download = getAndSetProviderJson(last_provider_main_url);
+ if(PROVIDER_JSON_DOWNLOADED || (current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY))) {
+ broadcast_progress(progress++);
+ PROVIDER_JSON_DOWNLOADED = true;
+ current_download = getAndSetEipServiceJson();
+ if(current_download.containsKey(RESULT_KEY) && current_download.getBoolean(RESULT_KEY)) {
+ broadcast_progress(progress++);
+ EIP_SERVICE_JSON_DOWNLOADED = true;
+ }
+ }
+ }
+
+ return current_download;
+ }
+
+ private Bundle downloadCACert(String provider_main_url, boolean danger_on) {
+ Bundle result = new Bundle();
+ String cert_string = downloadWithCommercialCA(provider_main_url + "/ca.crt", danger_on);
+
+ if(validCertificate(cert_string) && setting_up_provider) {
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.CA_CERT, cert_string).commit();
+ result.putBoolean(RESULT_KEY, true);
+ } else {
+ String reason_to_fail = pickErrorMessage(cert_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+
+ return result;
+ }
+
+
+ public static boolean caCertDownloaded() {
+ return CA_CERT_DOWNLOADED;
+ }
+
+ private boolean validCertificate(String cert_string) {
+ boolean result = false;
+ if(!ConfigHelper.checkErroneousDownload(cert_string)) {
+ X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(cert_string);
+ try {
+ Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT);
+ result = true;
+ } catch (CertificateEncodingException e) {
+ Log.d(TAG, e.getLocalizedMessage());
+ }
+ }
+
+ return result;
+ }
+
+ private Bundle getAndSetProviderJson(String provider_main_url) {
+ Bundle result = new Bundle();
+
+ if(setting_up_provider) {
+ String provider_dot_json_string = downloadWithProviderCA(provider_main_url + "/provider.json", true);
+
+ try {
+ JSONObject provider_json = new JSONObject(provider_dot_json_string);
+ String name = provider_json.getString(Provider.NAME);
+ //TODO setProviderName(name);
+
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(Provider.KEY, provider_json.toString()).commit();
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putBoolean(EIP.ALLOWED_ANON, provider_json.getJSONObject(Provider.SERVICE).getBoolean(EIP.ALLOWED_ANON)).commit();
+
+ result.putBoolean(RESULT_KEY, true);
+ } catch (JSONException e) {
+ //TODO Error message should be contained in that provider_dot_json_string
+ String reason_to_fail = pickErrorMessage(provider_dot_json_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+ }
+ return result;
+ }
+
+
+
+ public static boolean providerJsonDownloaded() {
+ return PROVIDER_JSON_DOWNLOADED;
+ }
+
+ private Bundle getAndSetEipServiceJson() {
+ Bundle result = new Bundle();
+ String eip_service_json_string = "";
+ if(setting_up_provider) {
+ try {
+ JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+ String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH;
+ eip_service_json_string = downloadWithProviderCA(eip_service_url, true);
+ JSONObject eip_service_json = new JSONObject(eip_service_json_string);
+ eip_service_json.getInt(Provider.API_RETURN_SERIAL);
+
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.KEY, eip_service_json.toString()).commit();
+
+ result.putBoolean(RESULT_KEY, true);
+ } catch (JSONException e) {
+ String reason_to_fail = pickErrorMessage(eip_service_json_string);
+ result.putString(ERRORS, reason_to_fail);
+ result.putBoolean(RESULT_KEY, false);
+ }
+ }
+ return result;
+ }
+
+ public static boolean eipServiceDownloaded() {
+ return EIP_SERVICE_JSON_DOWNLOADED;
+ }
+
+ /**
+ * Interprets the error message as a JSON object and extract the "errors" keyword pair.
+ * If the error message is not a JSON object, then it is returned untouched.
+ * @param string_json_error_message
+ * @return final error message
+ */
+ private String pickErrorMessage(String string_json_error_message) {
+ String error_message = "";
+ try {
+ JSONObject json_error_message = new JSONObject(string_json_error_message);
+ error_message = json_error_message.getString(ERRORS);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ error_message = string_json_error_message;
+ }
+
+ return error_message;
+ }
+
+ /**
+ * Tries to download the contents of the provided url using commercially validated CA certificate from chosen provider.
+ *
+ * If danger_on flag is true, SSL exceptions will be managed by futher methods that will try to use some bypass methods.
+ * @param string_url
+ * @param danger_on if the user completely trusts this provider
+ * @return
+ */
+ private String downloadWithCommercialCA(String string_url, boolean danger_on) {
+
+ String json_file_content = "";
+
+ URL provider_url = null;
+ int seconds_of_timeout = 1;
+ try {
+ provider_url = new URL(string_url);
+ URLConnection url_connection = provider_url.openConnection();
+ url_connection.setConnectTimeout(seconds_of_timeout*1000);
+ if(!LeapSRPSession.getToken().isEmpty())
+ url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken());
+ json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next();
+ } catch (MalformedURLException e) {
+ json_file_content = formatErrorMessage(R.string.malformed_url);
+ } catch(SocketTimeoutException e) {
+ json_file_content = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ if(provider_url != null) {
+ json_file_content = downloadWithProviderCA(string_url, danger_on);
+ } else {
+ json_file_content = formatErrorMessage(R.string.certificate_error);
+ }
+ } catch (Exception e) {
+ if(provider_url != null && danger_on) {
+ json_file_content = downloadWithProviderCA(string_url, danger_on);
+ }
+ }
+
+ return json_file_content;
+ }
+
+ /**
+ * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
+ * @param url as a string
+ * @param danger_on true to download CA certificate in case it has not been downloaded.
+ * @return an empty string if it fails, the url content if not.
+ */
+ private String downloadWithProviderCA(String url_string, boolean danger_on) {
+ String json_file_content = "";
+
+ try {
+ URL url = new URL(url_string);
+ // Tell the URLConnection to use a SocketFactory from our SSLContext
+ HttpsURLConnection urlConnection =
+ (HttpsURLConnection)url.openConnection();
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+ if(!LeapSRPSession.getToken().isEmpty())
+ urlConnection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
+ json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (UnknownHostException e) {
+ json_file_content = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ // The downloaded certificate doesn't validate our https connection.
+ if(danger_on) {
+ json_file_content = downloadWithoutCA(url_string);
+ } else {
+ json_file_content = formatErrorMessage(R.string.certificate_error);
+ }
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return json_file_content;
+ }
+
+ private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException {
+ String provider_cert_string = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.CA_CERT,"");
+
+ java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(provider_cert_string);
+
+ // Create a KeyStore containing our trusted CAs
+ String keyStoreType = KeyStore.getDefaultType();
+ KeyStore keyStore = KeyStore.getInstance(keyStoreType);
+ keyStore.load(null, null);
+ keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate);
+
+ // Create a TrustManager that trusts the CAs in our KeyStore
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init(keyStore);
+
+ // Create an SSLContext that uses our TrustManager
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, tmf.getTrustManagers(), null);
+
+ return context.getSocketFactory();
+ }
+
+ /**
+ * Downloads the string that's in the url with any certificate.
+ */
+ private String downloadWithoutCA(String url_string) {
+ String string = "";
+ try {
+
+ HostnameVerifier hostnameVerifier = new HostnameVerifier() {
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ };
+
+ class DefaultTrustManager implements X509TrustManager {
+
+ @Override
+ public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {}
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return null;
+ }
+ }
+
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(new KeyManager[0], new TrustManager[] {new DefaultTrustManager()}, new SecureRandom());
+
+ URL url = new URL(url_string);
+ HttpsURLConnection urlConnection = (HttpsURLConnection)url.openConnection();
+ urlConnection.setSSLSocketFactory(context.getSocketFactory());
+ urlConnection.setHostnameVerifier(hostnameVerifier);
+ string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next();
+ System.out.println("String ignoring certificate = " + string);
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ string = formatErrorMessage(R.string.server_unreachable_message);
+ } catch (IOException e) {
+ // The downloaded certificate doesn't validate our https connection.
+ e.printStackTrace();
+ string = formatErrorMessage(R.string.certificate_error);
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return string;
+ }
+
+ /**
+ * Logs out from the api url retrieved from the task.
+ * @param task containing api url from which the user will log out
+ * @return true if there were no exceptions
+ */
+ private boolean logOut(Bundle task) {
+ try {
+ String delete_url = task.getString(Provider.API_URL) + "/logout";
+ int progress = 0;
+
+ HttpsURLConnection urlConnection = (HttpsURLConnection)new URL(delete_url).openConnection();
+ urlConnection.setRequestMethod("DELETE");
+ urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory());
+
+ int responseCode = urlConnection.getResponseCode();
+ broadcast_progress(progress++);
+ LeapSRPSession.setToken("");
+ Log.d(TAG, Integer.toString(responseCode));
+ } catch (ClientProtocolException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (IndexOutOfBoundsException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (KeyManagementException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return true;
+ }
+
+ /**
+ * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate.
+ *
+ * @param task containing the type of the certificate to be downloaded
+ * @return true if certificate was downloaded correctly, false if provider.json or danger_on flag are not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error.
+ */
+ private boolean getNewCert(Bundle task) {
+
+ try {
+ String type_of_certificate = task.getString(ConfigurationWizard.TYPE_OF_CERTIFICATE);
+ JSONObject provider_json = new JSONObject(getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getString(Provider.KEY, ""));
+
+ String provider_main_url = provider_json.getString(Provider.API_URL);
+ URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.CERTIFICATE);
+
+ boolean danger_on = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).getBoolean(ProviderItem.DANGER_ON, false);
+
+ String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), danger_on);
+
+ if(!cert_string.isEmpty()) {
+ if(ConfigHelper.checkErroneousDownload(cert_string)) {
+ String reason_to_fail = provider_json.getString(ERRORS);
+ //result.putString(ConfigHelper.ERRORS_KEY, reason_to_fail);
+ //result.putBoolean(ConfigHelper.RESULT_KEY, false);
+ return false;
+ } else {
+
+ // API returns concatenated cert & key. Split them for OpenVPN options
+ String certificateString = null, keyString = null;
+ String[] certAndKey = cert_string.split("(?<=-\n)");
+ for (int i=0; i < certAndKey.length-1; i++){
+ if ( certAndKey[i].contains("KEY") ) {
+ keyString = certAndKey[i++] + certAndKey[i];
+ }
+ else if ( certAndKey[i].contains("CERTIFICATE") ) {
+ certificateString = certAndKey[i++] + certAndKey[i];
+ }
+ }
+ try {
+ RSAPrivateKey keyCert = ConfigHelper.parseRsaKeyFromString(keyString);
+ keyString = Base64.encodeToString( keyCert.getEncoded(), Base64.DEFAULT );
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n"+keyString+"-----END RSA PRIVATE KEY-----").commit();
+
+ X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString);
+ certificateString = Base64.encodeToString( certCert.getEncoded(), Base64.DEFAULT);
+ getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE).edit().putString(EIP.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n"+certificateString+"-----END CERTIFICATE-----").commit();
+
+ return true;
+ } catch (CertificateException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ } /*catch (URISyntaxException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ return false;
+ }*/
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
new file mode 100644
index 00000000..7b256124
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
+
+/**
+ * Implements the ResultReceiver needed by Activities using ProviderAPI to receive the results of its operations.
+ * @author parmegv
+ *
+ */
+public class ProviderAPIResultReceiver extends ResultReceiver {
+ private Receiver mReceiver;
+
+ public ProviderAPIResultReceiver(Handler handler) {
+ super(handler);
+ // TODO Auto-generated constructor stub
+ }
+
+ public void setReceiver(Receiver receiver) {
+ mReceiver = receiver;
+ }
+
+ /**
+ * Interface to enable ProviderAPIResultReceiver to receive results from the ProviderAPI IntentService.
+ * @author parmegv
+ *
+ */
+ public interface Receiver {
+ public void onReceiveResult(int resultCode, Bundle resultData);
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (mReceiver != null) {
+ mReceiver.onReceiveResult(resultCode, resultData);
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java b/app/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java
new file mode 100644
index 00000000..c067ce2b
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderDetailFragment.java
@@ -0,0 +1,115 @@
+package se.leap.bitmaskclient;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+public class ProviderDetailFragment extends DialogFragment {
+
+ final public static String TAG = "providerDetailFragment";
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ try {
+
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View provider_detail_view = inflater.inflate(R.layout.provider_detail_fragment, null);
+
+ JSONObject provider_json = new JSONObject(getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, getActivity().MODE_PRIVATE).getString(Provider.KEY, ""));
+
+ final TextView domain = (TextView)provider_detail_view.findViewById(R.id.provider_detail_domain);
+ domain.setText(provider_json.getString(Provider.DOMAIN));
+ final TextView name = (TextView)provider_detail_view.findViewById(R.id.provider_detail_name);
+ name.setText(provider_json.getJSONObject(Provider.NAME).getString("en"));
+ final TextView description = (TextView)provider_detail_view.findViewById(R.id.provider_detail_description);
+ description.setText(provider_json.getJSONObject(Provider.DESCRIPTION).getString("en"));
+
+ builder.setView(provider_detail_view);
+ builder.setTitle(R.string.provider_details_fragment_title);
+
+ if(anon_allowed(provider_json)) {
+ builder.setPositiveButton(R.string.use_anonymously_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_configuration_wizard.use_anonymously();
+ }
+ });
+ }
+
+ if(registration_allowed(provider_json)) {
+ builder.setNegativeButton(R.string.login_button, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_configuration_wizard.login();
+ }
+ });
+ }
+
+ return builder.create();
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+
+ private boolean anon_allowed(JSONObject provider_json) {
+ try {
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+ return service_description.has(EIP.ALLOWED_ANON) && service_description.getBoolean(EIP.ALLOWED_ANON);
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ private boolean registration_allowed(JSONObject provider_json) {
+ try {
+ JSONObject service_description = provider_json.getJSONObject(Provider.SERVICE);
+ return service_description.has(Provider.ALLOW_REGISTRATION) && service_description.getBoolean(Provider.ALLOW_REGISTRATION);
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ super.onCancel(dialog);
+ SharedPreferences.Editor editor = getActivity().getSharedPreferences(Dashboard.SHARED_PREFERENCES, Activity.MODE_PRIVATE).edit();
+ editor.remove(Provider.KEY).remove(ProviderItem.DANGER_ON).remove(EIP.ALLOWED_ANON).remove(EIP.KEY).commit();
+ interface_with_configuration_wizard.showAllProviders();
+ }
+
+ public static DialogFragment newInstance() {
+ ProviderDetailFragment provider_detail_fragment = new ProviderDetailFragment();
+ return provider_detail_fragment;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ interface_with_configuration_wizard = (ProviderDetailFragmentInterface) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(activity.toString()
+ + " must implement LogInDialogListener");
+ }
+ }
+
+ public interface ProviderDetailFragmentInterface {
+ public void login();
+ public void use_anonymously();
+ public void showAllProviders();
+ }
+
+ ProviderDetailFragmentInterface interface_with_configuration_wizard;
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java
new file mode 100644
index 00000000..43bba085
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java
@@ -0,0 +1,114 @@
+package se.leap.bitmaskclient;
+
+import java.util.List;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TwoLineListItem;
+
+public class ProviderListAdapter<T> extends ArrayAdapter<T> {
+ private static boolean[] hidden = null;
+
+ public void hide(int position) {
+ hidden[getRealPosition(position)] = true;
+ notifyDataSetChanged();
+ notifyDataSetInvalidated();
+ }
+
+ public void unHide(int position) {
+ hidden[getRealPosition(position)] = false;
+ notifyDataSetChanged();
+ notifyDataSetInvalidated();
+ }
+
+ public void unHideAll() {
+ for (int provider_index = 0; provider_index < hidden.length; provider_index++)
+ hidden[provider_index] = false;
+ }
+
+ private int getRealPosition(int position) {
+ int hElements = getHiddenCountUpTo(position);
+ int diff = 0;
+ for(int i=0;i<hElements;i++) {
+ diff++;
+ if(hidden[position+diff])
+ i--;
+ }
+ return (position + diff);
+ }
+ private int getHiddenCount() {
+ int count = 0;
+ for(int i=0;i<hidden.length;i++)
+ if(hidden[i])
+ count++;
+ return count;
+ }
+ private int getHiddenCountUpTo(int location) {
+ int count = 0;
+ for(int i=0;i<=location;i++) {
+ if(hidden[i])
+ count++;
+ }
+ return count;
+ }
+
+ @Override
+ public int getCount() {
+ return (hidden.length - getHiddenCount());
+ }
+
+ public ProviderListAdapter(Context mContext, int layout, List<T> objects) {
+ super(mContext, layout, objects);
+ if(hidden == null) {
+ hidden = new boolean[objects.size()];
+ for (int i = 0; i < objects.size(); i++)
+ hidden[i] = false;
+ }
+ }
+
+ public ProviderListAdapter(Context mContext, int layout, List<T> objects, boolean show_all_providers) {
+ super(mContext, layout, objects);
+ if(show_all_providers) {
+ hidden = new boolean[objects.size()];
+ for (int i = 0; i < objects.size(); i++)
+ hidden[i] = false;
+ }
+ }
+
+ @Override
+ public void add(T item) {
+ super.add(item);
+ boolean[] new_hidden = new boolean[hidden.length+1];
+ System.arraycopy(hidden, 0, new_hidden, 0, hidden.length);
+ new_hidden[hidden.length] = false;
+ hidden = new_hidden;
+ }
+
+ @Override
+ public void remove(T item) {
+ super.remove(item);
+ boolean[] new_hidden = new boolean[hidden.length-1];
+ System.arraycopy(hidden, 0, new_hidden, 0, hidden.length-1);
+ hidden = new_hidden;
+ }
+
+ @Override
+ public View getView(int index, View convertView, ViewGroup parent) {
+ TwoLineListItem row;
+ int position = getRealPosition(index);
+ if (convertView == null) {
+ LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ row = (TwoLineListItem)inflater.inflate(R.layout.provider_list_item, null);
+ } else {
+ row = (TwoLineListItem)convertView;
+ }
+ ProviderListContent.ProviderItem data = ProviderListContent.ITEMS.get(position);
+ row.getText1().setText(data.domain());
+ row.getText2().setText(data.name());
+
+ return row;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListContent.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListContent.java
new file mode 100644
index 00000000..e1ca4f9a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderListContent.java
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.net.URL;
+import java.net.MalformedURLException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Models the provider list shown in the ConfigurationWizard.
+ *
+ * @author parmegv
+ *
+ */
+public class ProviderListContent {
+
+ public static List<ProviderItem> ITEMS = new ArrayList<ProviderItem>();
+
+ public static Map<String, ProviderItem> ITEM_MAP = new HashMap<String, ProviderItem>();
+
+ /**
+ * Adds a new provider item to the end of the items map, and to the items list.
+ * @param item
+ */
+ public static void addItem(ProviderItem item) {
+ ITEMS.add(item);
+ ITEM_MAP.put(String.valueOf(ITEMS.size()), item);
+ }
+ public static void removeItem(ProviderItem item) {
+ ITEMS.remove(item);
+ ITEM_MAP.remove(item);
+ }
+
+ /**
+ * A provider item.
+ */
+ public static class ProviderItem {
+ final public static String CUSTOM = "custom";
+ final public static String DANGER_ON = "danger_on";
+ private String provider_main_url;
+ private String name;
+
+ /**
+ * @param name of the provider
+ * @param urls_file_input_stream file input stream linking with the assets url file
+ * @param custom if it's a new provider entered by the user or not
+ * @param danger_on if the user trusts completely the new provider
+ */
+ public ProviderItem(String name, InputStream urls_file_input_stream) {
+
+ try {
+ byte[] urls_file_bytes = new byte[urls_file_input_stream.available()];
+ urls_file_input_stream.read(urls_file_bytes);
+ String urls_file_content = new String(urls_file_bytes);
+ JSONObject file_contents = new JSONObject(urls_file_content);
+ provider_main_url = file_contents.getString(Provider.MAIN_URL);
+ this.name = name;
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * @param name of the provider
+ * @param provider_main_url used to download provider.json file of the provider
+ * @param provider_json already downloaded
+ * @param custom if it's a new provider entered by the user or not
+ */
+ public ProviderItem(String name, String provider_main_url) {
+ this.name = name;
+ this.provider_main_url = provider_main_url;
+ }
+
+ public String name() { return name; }
+
+ public String providerMainUrl() { return provider_main_url; }
+
+ public String domain() {
+ try {
+ return new URL(provider_main_url).getHost();
+ } catch (MalformedURLException e) {
+ return provider_main_url.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java b/app/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java
new file mode 100644
index 00000000..db414d87
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java
@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+ package se.leap.bitmaskclient;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
+import android.app.Activity;
+import android.app.ListFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+/**
+ * A list fragment representing a list of Providers. This fragment
+ * also supports tablet devices by allowing list items to be given an
+ * 'activated' state upon selection. This helps indicate which item is
+ * currently being viewed in a {@link DashboardFragment}.
+ * <p>
+ * Activities containing this fragment MUST implement the {@link Callbacks}
+ * interface.
+ */
+public class ProviderListFragment extends ListFragment {
+
+ public static String TAG = "provider_list_fragment";
+ public static String SHOW_ALL_PROVIDERS = "show_all_providers";
+ public static String TOP_PADDING = "top padding from providerlistfragment";
+ private ProviderListAdapter<ProviderItem> content_adapter;
+
+ /**
+ * The serialization (saved instance state) Bundle key representing the
+ * activated item position. Only used on tablets.
+ */
+ private static final String STATE_ACTIVATED_POSITION = "activated_position";
+
+ /**
+ * The fragment's current callback object, which is notified of list item
+ * clicks.
+ */
+ private Callbacks mCallbacks = sDummyCallbacks;
+
+ /**
+ * The current activated item position. Only used on tablets.
+ */
+ private int mActivatedPosition = ListView.INVALID_POSITION;
+
+ /**
+ * A callback interface that all activities containing this fragment must
+ * implement. This mechanism allows activities to be notified of item
+ * selections.
+ */
+ public interface Callbacks {
+ /**
+ * Callback for when an item has been selected.
+ */
+ public void onItemSelected(String id);
+ }
+
+ /**
+ * A dummy implementation of the {@link Callbacks} interface that does
+ * nothing. Used only when this fragment is not attached to an activity.
+ */
+ private static Callbacks sDummyCallbacks = new Callbacks() {
+ @Override
+ public void onItemSelected(String id) {
+ }
+ };
+
+ /**
+ * Mandatory empty constructor for the fragment manager to instantiate the
+ * fragment (e.g. upon screen orientation changes).
+ */
+ public ProviderListFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if(getArguments().containsKey(SHOW_ALL_PROVIDERS))
+ content_adapter = new ProviderListAdapter<ProviderListContent.ProviderItem>(
+ getActivity(),
+ R.layout.provider_list_item,
+ ProviderListContent.ITEMS, getArguments().getBoolean(SHOW_ALL_PROVIDERS));
+ else
+ content_adapter = new ProviderListAdapter<ProviderListContent.ProviderItem>(
+ getActivity(),
+ R.layout.provider_list_item,
+ ProviderListContent.ITEMS);
+
+
+ setListAdapter(content_adapter);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ return inflater.inflate(R.layout.provider_list_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // Restore the previously serialized activated item position.
+ if (savedInstanceState != null
+ && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) {
+ setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION));
+ }
+ if(getArguments() != null && getArguments().containsKey(TOP_PADDING)) {
+ int topPadding = getArguments().getInt(TOP_PADDING);
+ View current_view = getView();
+ getView().setPadding(current_view.getPaddingLeft(), topPadding, current_view.getPaddingRight(), current_view.getPaddingBottom());
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ // Activities containing this fragment must implement its callbacks.
+ if (!(activity instanceof Callbacks)) {
+ throw new IllegalStateException("Activity must implement fragment's callbacks.");
+ }
+
+ mCallbacks = (Callbacks) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+
+ // Reset the active callbacks interface to the dummy implementation.
+ mCallbacks = sDummyCallbacks;
+ }
+
+ @Override
+ public void onListItemClick(ListView listView, View view, int position, long id) {
+ super.onListItemClick(listView, view, position, id);
+
+ // Notify the active callbacks interface (the activity, if the
+ // fragment is attached to one) that an item has been selected.
+ mCallbacks.onItemSelected(ProviderListContent.ITEMS.get(position).name());
+
+ for(int item_position = 0; item_position < listView.getCount(); item_position++) {
+ if(item_position != position)
+ content_adapter.hide(item_position);
+ }
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mActivatedPosition != ListView.INVALID_POSITION) {
+ // Serialize and persist the activated item position.
+ outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition);
+ }
+ }
+
+ public void notifyAdapter() {
+ content_adapter.notifyDataSetChanged();
+ }
+ /**
+ * Turns on activate-on-click mode. When this mode is on, list items will be
+ * given the 'activated' state when touched.
+ */
+ public void setActivateOnItemClick(boolean activateOnItemClick) {
+ // When setting CHOICE_MODE_SINGLE, ListView will automatically
+ // give items the 'activated' state when touched.
+ getListView().setChoiceMode(activateOnItemClick
+ ? ListView.CHOICE_MODE_SINGLE
+ : ListView.CHOICE_MODE_NONE);
+ }
+
+ private void setActivatedPosition(int position) {
+ if (position == ListView.INVALID_POSITION) {
+ getListView().setItemChecked(mActivatedPosition, false);
+ } else {
+ getListView().setItemChecked(position, true);
+ }
+
+ mActivatedPosition = position;
+ }
+
+ public void removeLastItem() {
+ unhideAll();
+ content_adapter.remove(content_adapter.getItem(content_adapter.getCount()-1));
+ content_adapter.notifyDataSetChanged();
+ }
+
+ public void addItem(ProviderItem provider) {
+ content_adapter.add(provider);
+ content_adapter.notifyDataSetChanged();
+ }
+
+ public void hideAllBut(int position) {
+ int real_count = content_adapter.getCount();
+ for(int i = 0; i < real_count;)
+ if(i != position) {
+ content_adapter.hide(i);
+ position--;
+ real_count--;
+ } else {
+ i++;
+ }
+ }
+
+ public void unhideAll() {
+ if(content_adapter != null) {
+ content_adapter.unHideAll();
+ content_adapter.notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * @return a new instance of this ListFragment.
+ */
+ public static ProviderListFragment newInstance() {
+ return new ProviderListFragment();
+ }
+}
diff --git a/app/src/main/java/se/leap/openvpn/CIDRIP.java b/app/src/main/java/se/leap/openvpn/CIDRIP.java
new file mode 100644
index 00000000..8c4b6709
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/CIDRIP.java
@@ -0,0 +1,58 @@
+package se.leap.openvpn;
+
+class CIDRIP{
+ String mIp;
+ int len;
+ public CIDRIP(String ip, String mask){
+ mIp=ip;
+ long netmask=getInt(mask);
+
+ // Add 33. bit to ensure the loop terminates
+ netmask += 1l << 32;
+
+ int lenZeros = 0;
+ while((netmask & 0x1) == 0) {
+ lenZeros++;
+ netmask = netmask >> 1;
+ }
+ // Check if rest of netmask is only 1s
+ if(netmask != (0x1ffffffffl >> lenZeros)) {
+ // Asume no CIDR, set /32
+ len=32;
+ } else {
+ len =32 -lenZeros;
+ }
+
+ }
+ @Override
+ public String toString() {
+ return String.format("%s/%d",mIp,len);
+ }
+
+ public boolean normalise(){
+ long ip=getInt(mIp);
+
+ long newip = ip & (0xffffffffl << (32 -len));
+ if (newip != ip){
+ mIp = String.format("%d.%d.%d.%d", (newip & 0xff000000) >> 24,(newip & 0xff0000) >> 16, (newip & 0xff00) >> 8 ,newip & 0xff);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ static long getInt(String ipaddr) {
+ String[] ipt = ipaddr.split("\\.");
+ long ip=0;
+
+ ip += Long.parseLong(ipt[0])<< 24;
+ ip += Integer.parseInt(ipt[1])<< 16;
+ ip += Integer.parseInt(ipt[2])<< 8;
+ ip += Integer.parseInt(ipt[3]);
+
+ return ip;
+ }
+ public long getInt() {
+ return getInt(mIp);
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/openvpn/ConfigParser.java b/app/src/main/java/se/leap/openvpn/ConfigParser.java
new file mode 100644
index 00000000..df4eae1b
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/ConfigParser.java
@@ -0,0 +1,569 @@
+package se.leap.openvpn;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Vector;
+
+//! Openvpn Config FIle Parser, probably not 100% accurate but close enough
+
+// And rember, this is valid :)
+// --<foo>
+// bar
+// </foo>
+public class ConfigParser {
+
+
+ private HashMap<String, Vector<Vector<String>>> options = new HashMap<String, Vector<Vector<String>>>();
+ public void parseConfig(Reader reader) throws IOException, ConfigParseError {
+
+
+ BufferedReader br =new BufferedReader(reader);
+
+ @SuppressWarnings("unused")
+ int lineno=0;
+
+ while (true){
+ String line = br.readLine();
+ if(line==null)
+ break;
+ lineno++;
+ Vector<String> args = parseline(line);
+ if(args.size() ==0)
+ continue;
+
+
+ if(args.get(0).startsWith("--"))
+ args.set(0, args.get(0).substring(2));
+
+ checkinlinefile(args,br);
+
+ String optionname = args.get(0);
+ if(!options.containsKey(optionname)) {
+ options.put(optionname, new Vector<Vector<String>>());
+ }
+ options.get(optionname).add(args);
+ }
+ }
+ public void setDefinition(HashMap<String,Vector<Vector<String>>> args) {
+ options = args;
+ }
+
+ private void checkinlinefile(Vector<String> args, BufferedReader br) throws IOException, ConfigParseError {
+ String arg0 = args.get(0);
+ // CHeck for <foo>
+ if(arg0.startsWith("<") && arg0.endsWith(">")) {
+ String argname = arg0.substring(1, arg0.length()-1);
+ String inlinefile = VpnProfile.INLINE_TAG;
+
+ String endtag = String.format("</%s>",argname);
+ do {
+ String line = br.readLine();
+ if(line==null){
+ throw new ConfigParseError(String.format("No endtag </%s> for starttag <%s> found",argname,argname));
+ }
+ if(line.equals(endtag))
+ break;
+ else {
+ inlinefile+=line;
+ inlinefile+= "\n";
+ }
+ } while(true);
+
+ args.clear();
+ args.add(argname);
+ args.add(inlinefile);
+ }
+
+ }
+
+ enum linestate {
+ initial,
+ readin_single_quote
+ , reading_quoted, reading_unquoted, done}
+
+ private boolean space(char c) {
+ // I really hope nobody is using zero bytes inside his/her config file
+ // to sperate parameter but here we go:
+ return Character.isWhitespace(c) || c == '\0';
+
+ }
+
+ public class ConfigParseError extends Exception {
+ private static final long serialVersionUID = -60L;
+
+ public ConfigParseError(String msg) {
+ super(msg);
+ }
+ }
+
+
+ // adapted openvpn's parse function to java
+ private Vector<String> parseline(String line) throws ConfigParseError {
+ Vector<String> parameters = new Vector<String>();
+
+ if (line.length()==0)
+ return parameters;
+
+
+ linestate state = linestate.initial;
+ boolean backslash = false;
+ char out=0;
+
+ int pos=0;
+ String currentarg="";
+
+ do {
+ // Emulate the c parsing ...
+ char in;
+ if(pos < line.length())
+ in = line.charAt(pos);
+ else
+ in = '\0';
+
+ if (!backslash && in == '\\' && state != linestate.readin_single_quote)
+ {
+ backslash = true;
+ }
+ else
+ {
+ if (state == linestate.initial)
+ {
+ if (!space (in))
+ {
+ if (in == ';' || in == '#') /* comment */
+ break;
+ if (!backslash && in == '\"')
+ state = linestate.reading_quoted;
+ else if (!backslash && in == '\'')
+ state = linestate.readin_single_quote;
+ else
+ {
+ out = in;
+ state = linestate.reading_unquoted;
+ }
+ }
+ }
+ else if (state == linestate.reading_unquoted)
+ {
+ if (!backslash && space (in))
+ state = linestate.done;
+ else
+ out = in;
+ }
+ else if (state == linestate.reading_quoted)
+ {
+ if (!backslash && in == '\"')
+ state = linestate.done;
+ else
+ out = in;
+ }
+ else if (state == linestate.readin_single_quote)
+ {
+ if (in == '\'')
+ state = linestate.done;
+ else
+ out = in;
+ }
+
+ if (state == linestate.done)
+ {
+ /* ASSERT (parm_len > 0); */
+ state = linestate.initial;
+ parameters.add(currentarg);
+ currentarg = "";
+ out =0;
+ }
+
+ if (backslash && out!=0)
+ {
+ if (!(out == '\\' || out == '\"' || space (out)))
+ {
+ throw new ConfigParseError("Options warning: Bad backslash ('\\') usage");
+ }
+ }
+ backslash = false;
+ }
+
+ /* store parameter character */
+ if (out!=0)
+ {
+ currentarg+=out;
+ }
+ } while (pos++ < line.length());
+
+ return parameters;
+ }
+
+
+ final String[] unsupportedOptions = { "config",
+ "connection",
+ "proto-force",
+ "remote-random",
+ "tls-server"
+
+ };
+
+ // Ignore all scripts
+ // in most cases these won't work and user who wish to execute scripts will
+ // figure out themselves
+ final String[] ignoreOptions = { "tls-client",
+ "askpass",
+ "auth-nocache",
+ "up",
+ "down",
+ "route-up",
+ "ipchange",
+ "route-up",
+ "route-pre-down",
+ "auth-user-pass-verify",
+ "dhcp-release",
+ "dhcp-renew",
+ "dh",
+ "management-hold",
+ "management",
+ "management-query-passwords",
+ "pause-exit",
+ "persist-key",
+ "register-dns",
+ "route-delay",
+ "route-gateway",
+ "route-metric",
+ "route-method",
+ "status",
+ "script-security",
+ "show-net-up",
+ "suppress-timestamps",
+ "tmp-dir",
+ "tun-ipv6",
+ "topology",
+ "win-sys",
+ };
+
+
+ // This method is far too long
+ public VpnProfile convertProfile() throws ConfigParseError{
+ boolean noauthtypeset=true;
+ VpnProfile np = new VpnProfile("converted Profile");
+ // Pull, client, tls-client
+ np.clearDefaults();
+
+ // XXX we are always client
+ if(/*options.containsKey("client") || options.containsKey("pull")*/ true) {
+ np.mUsePull=true;
+ options.remove("pull");
+ options.remove("client");
+ }
+
+ Vector<String> secret = getOption("secret", 1, 2);
+ if(secret!=null)
+ {
+ np.mAuthenticationType=VpnProfile.TYPE_STATICKEYS;
+ noauthtypeset=false;
+ np.mUseTLSAuth=true;
+ np.mTLSAuthFilename=secret.get(1);
+ if(secret.size()==3)
+ np.mTLSAuthDirection=secret.get(2);
+
+ }
+
+ Vector<Vector<String>> routes = getAllOption("route", 1, 4);
+ if(routes!=null) {
+ String routeopt = "";
+ for(Vector<String> route:routes){
+ String netmask = "255.255.255.255";
+ if(route.size() >= 3)
+ netmask = route.get(2);
+ String net = route.get(1);
+ try {
+ CIDRIP cidr = new CIDRIP(net, netmask);
+ routeopt+=cidr.toString() + " ";
+ } catch (ArrayIndexOutOfBoundsException aioob) {
+ throw new ConfigParseError("Could not parse netmask of route " + netmask);
+ } catch (NumberFormatException ne) {
+ throw new ConfigParseError("Could not parse netmask of route " + netmask);
+ }
+
+ }
+ np.mCustomRoutes=routeopt;
+ }
+
+ // Also recognize tls-auth [inline] direction ...
+ Vector<Vector<String>> tlsauthoptions = getAllOption("tls-auth", 1, 2);
+ if(tlsauthoptions!=null) {
+ for(Vector<String> tlsauth:tlsauthoptions) {
+ if(tlsauth!=null)
+ {
+ if(!tlsauth.get(1).equals("[inline]")) {
+ np.mTLSAuthFilename=tlsauth.get(1);
+ np.mUseTLSAuth=true;
+ }
+ if(tlsauth.size()==3)
+ np.mTLSAuthDirection=tlsauth.get(2);
+ }
+ }
+ }
+
+ Vector<String> direction = getOption("key-direction", 1, 1);
+ if(direction!=null)
+ np.mTLSAuthDirection=direction.get(1);
+
+
+ if(getAllOption("redirect-gateway", 0, 5) != null)
+ np.mUseDefaultRoute=true;
+
+ Vector<String> dev =getOption("dev",1,1);
+ Vector<String> devtype =getOption("dev-type",1,1);
+
+ if( (devtype !=null && devtype.get(1).equals("tun")) ||
+ (dev!=null && dev.get(1).startsWith("tun")) ||
+ (devtype ==null && dev == null) ) {
+ //everything okay
+ } else {
+ throw new ConfigParseError("Sorry. Only tun mode is supported. See the FAQ for more detail");
+ }
+
+
+
+ Vector<String> mode =getOption("mode",1,1);
+ if (mode != null){
+ if(!mode.get(1).equals("p2p"))
+ throw new ConfigParseError("Invalid mode for --mode specified, need p2p");
+ }
+
+ Vector<String> port = getOption("port", 1,1);
+ if(port!=null){
+ np.mServerPort = port.get(1);
+ }
+
+ Vector<String> proto = getOption("proto", 1,1);
+ if(proto!=null){
+ np.mUseUdp=isUdpProto(proto.get(1));;
+ }
+
+ // Parse remote config
+ Vector<String> remote = getOption("remote",1,3);
+ if(remote != null){
+ switch (remote.size()) {
+ case 4:
+ np.mUseUdp=isUdpProto(remote.get(3));
+ case 3:
+ np.mServerPort = remote.get(2);
+ case 2:
+ np.mServerName = remote.get(1);
+ }
+ }
+
+ // Parse remote config
+ Vector<String> location = getOption("location",0,2);
+ if(location != null && location.size() == 2){
+ np.mLocation = location.get(1).replace("__", ", ");
+ }
+
+ Vector<Vector<String>> dhcpoptions = getAllOption("dhcp-option", 2, 2);
+ if(dhcpoptions!=null) {
+ for(Vector<String> dhcpoption:dhcpoptions) {
+ String type=dhcpoption.get(1);
+ String arg = dhcpoption.get(2);
+ if(type.equals("DOMAIN")) {
+ np.mSearchDomain=dhcpoption.get(2);
+ } else if(type.equals("DNS")) {
+ np.mOverrideDNS=true;
+ if(np.mDNS1.equals(VpnProfile.DEFAULT_DNS1))
+ np.mDNS1=arg;
+ else
+ np.mDNS2=arg;
+ }
+ }
+ }
+
+ Vector<String> ifconfig = getOption("ifconfig", 2, 2);
+ if(ifconfig!=null) {
+ CIDRIP cidr = new CIDRIP(ifconfig.get(1), ifconfig.get(2));
+ np.mIPv4Address=cidr.toString();
+ }
+
+ if(getOption("remote-random-hostname", 0, 0)!=null)
+ np.mUseRandomHostname=true;
+
+ if(getOption("float", 0, 0)!=null)
+ np.mUseFloat=true;
+
+ if(getOption("comp-lzo", 0, 1)!=null)
+ np.mUseLzo=true;
+
+ Vector<String> cipher = getOption("cipher", 1, 1);
+ if(cipher!=null)
+ np.mCipher= cipher.get(1);
+
+ Vector<String> ca = getOption("ca",1,1);
+ if(ca!=null){
+ np.mCaFilename = ca.get(1);
+ }
+
+ Vector<String> cert = getOption("cert",1,1);
+ if(cert!=null){
+ np.mClientCertFilename = cert.get(1);
+ np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES;
+ noauthtypeset=false;
+ }
+ Vector<String> key= getOption("key",1,1);
+ if(key!=null)
+ np.mClientKeyFilename=key.get(1);
+
+ Vector<String> pkcs12 = getOption("pkcs12",1,1);
+ if(pkcs12!=null) {
+ np.mPKCS12Filename = pkcs12.get(1);
+ np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE;
+ noauthtypeset=false;
+ }
+
+ Vector<String> tlsremote = getOption("tls-remote",1,1);
+ if(tlsremote!=null){
+ np.mRemoteCN = tlsremote.get(1);
+ np.mCheckRemoteCN=true;
+ }
+
+ Vector<String> verb = getOption("verb",1,1);
+ if(verb!=null){
+ np.mVerb=verb.get(1);
+ }
+
+
+ if(getOption("nobind", 0, 0) != null)
+ np.mNobind=true;
+
+ if(getOption("persist-tun", 0,0) != null)
+ np.mPersistTun=true;
+
+ Vector<String> connectretry = getOption("connect-retry", 1, 1);
+ if(connectretry!=null)
+ np.mConnectRetry =connectretry.get(1);
+
+ Vector<String> connectretrymax = getOption("connect-retry-max", 1, 1);
+ if(connectretrymax!=null)
+ np.mConnectRetryMax =connectretrymax.get(1);
+
+ Vector<Vector<String>> remotetls = getAllOption("remote-cert-tls", 1, 1);
+ if(remotetls!=null)
+ if(remotetls.get(0).get(1).equals("server"))
+ np.mExpectTLSCert=true;
+ else
+ options.put("remotetls",remotetls);
+
+ Vector<String> authuser = getOption("auth-user-pass",0,1);
+ if(authuser !=null){
+ if(noauthtypeset) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS;
+ } else if(np.mAuthenticationType==VpnProfile.TYPE_CERTIFICATES) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS_CERTIFICATES;
+ } else if(np.mAuthenticationType==VpnProfile.TYPE_KEYSTORE) {
+ np.mAuthenticationType=VpnProfile.TYPE_USERPASS_KEYSTORE;
+ }
+ if(authuser.size()>1) {
+ // Set option value to password get to get canche to embed later.
+ np.mUsername=null;
+ np.mPassword=authuser.get(1);
+ useEmbbedUserAuth(np,authuser.get(1));
+ }
+
+ }
+
+
+ // Check the other options
+
+ checkIgnoreAndInvalidOptions(np);
+ fixup(np);
+
+ return np;
+ }
+
+ private boolean isUdpProto(String proto) throws ConfigParseError {
+ boolean isudp;
+ if(proto.equals("udp") || proto.equals("udp6"))
+ isudp=true;
+ else if (proto.equals("tcp-client") ||
+ proto.equals("tcp") ||
+ proto.equals("tcp6") ||
+ proto.endsWith("tcp6-client"))
+ isudp =false;
+ else
+ throw new ConfigParseError("Unsupported option to --proto " + proto);
+ return isudp;
+ }
+
+ static public void useEmbbedUserAuth(VpnProfile np,String inlinedata)
+ {
+ String data = inlinedata.replace(VpnProfile.INLINE_TAG, "");
+ String[] parts = data.split("\n");
+ if(parts.length >= 2) {
+ np.mUsername=parts[0];
+ np.mPassword=parts[1];
+ }
+ }
+
+ private void checkIgnoreAndInvalidOptions(VpnProfile np) throws ConfigParseError {
+ for(String option:unsupportedOptions)
+ if(options.containsKey(option))
+ throw new ConfigParseError(String.format("Unsupported Option %s encountered in config file. Aborting",option));
+
+ for(String option:ignoreOptions)
+ // removing an item which is not in the map is no error
+ options.remove(option);
+
+ if(options.size()> 0) {
+ String custom = "# These Options were found in the config file do not map to config settings:\n";
+
+ for(Vector<Vector<String>> option:options.values()) {
+ for(Vector<String> optionsline: option) {
+ for (String arg : optionsline)
+ custom+= VpnProfile.openVpnEscape(arg) + " ";
+ }
+ custom+="\n";
+
+ }
+ np.mCustomConfigOptions = custom;
+ np.mUseCustomConfig=true;
+
+ }
+ }
+
+
+ private void fixup(VpnProfile np) {
+ if(np.mRemoteCN.equals(np.mServerName)) {
+ np.mRemoteCN="";
+ }
+ }
+
+ private Vector<String> getOption(String option, int minarg, int maxarg) throws ConfigParseError {
+ Vector<Vector<String>> alloptions = getAllOption(option, minarg, maxarg);
+ if(alloptions==null)
+ return null;
+ else
+ return alloptions.lastElement();
+ }
+
+
+ private Vector<Vector<String>> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError {
+ Vector<Vector<String>> args = options.get(option);
+ if(args==null)
+ return null;
+
+ for(Vector<String> optionline:args)
+
+ if(optionline.size()< (minarg+1) || optionline.size() > maxarg+1) {
+ String err = String.format(Locale.getDefault(),"Option %s has %d parameters, expected between %d and %d",
+ option,optionline.size()-1,minarg,maxarg );
+ throw new ConfigParseError(err);
+ }
+ options.remove(option);
+ return args;
+ }
+
+}
+
+
+
+
diff --git a/app/src/main/java/se/leap/openvpn/LICENSE.txt b/app/src/main/java/se/leap/openvpn/LICENSE.txt
new file mode 100644
index 00000000..d897edea
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/LICENSE.txt
@@ -0,0 +1,24 @@
+License for OpenVPN for Android. Please note that the thirdparty libraries/executables may have other license (OpenVPN, lzo, OpenSSL, Google Breakpad)
+
+Copyright (c) 2012-2013, Arne Schwabe
+ All rights reserved.
+
+If you need a non GPLv2 license of the source please contact me.
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+In addition, as a special exception, the copyright holders give
+permission to link the code of portions of this program with the
+OpenSSL library.
diff --git a/app/src/main/java/se/leap/openvpn/LaunchVPN.java b/app/src/main/java/se/leap/openvpn/LaunchVPN.java
new file mode 100644
index 00000000..89f2d372
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/LaunchVPN.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package se.leap.openvpn;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.EIP;
+import se.leap.bitmaskclient.R;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ListActivity;
+import android.content.ActivityNotFoundException;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.VpnService;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.preference.PreferenceManager;
+import android.text.InputType;
+import android.text.method.PasswordTransformationMethod;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+
+/**
+ * This Activity actually handles two stages of a launcher shortcut's life cycle.
+ *
+ * 1. Your application offers to provide shortcuts to the launcher. When
+ * the user installs a shortcut, an activity within your application
+ * generates the actual shortcut and returns it to the launcher, where it
+ * is shown to the user as an icon.
+ *
+ * 2. Any time the user clicks on an installed shortcut, an intent is sent.
+ * Typically this would then be handled as necessary by an activity within
+ * your application.
+ *
+ * We handle stage 1 (creating a shortcut) by simply sending back the information (in the form
+ * of an {@link android.content.Intent} that the launcher will use to create the shortcut.
+ *
+ * You can also implement this in an interactive way, by having your activity actually present
+ * UI for the user to select the specific nature of the shortcut, such as a contact, picture, URL,
+ * media item, or action.
+ *
+ * We handle stage 2 (responding to a shortcut) in this sample by simply displaying the contents
+ * of the incoming {@link android.content.Intent}.
+ *
+ * In a real application, you would probably use the shortcut intent to display specific content
+ * or start a particular operation.
+ */
+public class LaunchVPN extends ListActivity implements OnItemClickListener {
+
+ public static final String EXTRA_KEY = "se.leap.openvpn.shortcutProfileUUID";
+ public static final String EXTRA_NAME = "se.leap.openvpn.shortcutProfileName";
+ public static final String EXTRA_HIDELOG = "se.leap.openvpn.showNoLogWindow";;
+
+ public static final int START_VPN_PROFILE= 70;
+
+ // Dashboard, maybe more, want to know!
+ private ResultReceiver mReceiver;
+
+ private ProfileManager mPM;
+ private VpnProfile mSelectedProfile;
+ private boolean mhideLog=false;
+
+ private boolean mCmfixed=false;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPM =ProfileManager.getInstance(this);
+
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Resolve the intent
+
+ final Intent intent = getIntent();
+ final String action = intent.getAction();
+
+ // If something wants feedback, they sent us a Receiver
+ mReceiver = intent.getParcelableExtra(EIP.RECEIVER_TAG);
+
+ // If the intent is a request to create a shortcut, we'll do that and exit
+
+
+ if(Intent.ACTION_MAIN.equals(action)) {
+ // we got called to be the starting point, most likely a shortcut
+ String shortcutUUID = intent.getStringExtra( EXTRA_KEY);
+ String shortcutName = intent.getStringExtra( EXTRA_NAME);
+ mhideLog = intent.getBooleanExtra(EXTRA_HIDELOG, false);
+
+ VpnProfile profileToConnect = ProfileManager.get(shortcutUUID);
+ if(shortcutName != null && profileToConnect ==null)
+ profileToConnect = ProfileManager.getInstance(this).getProfileByName(shortcutName);
+
+ if(profileToConnect ==null) {
+ OpenVPN.logError(R.string.shortcut_profile_notfound);
+ // show Log window to display error
+ showLogWindow();
+ finish();
+ return;
+ }
+
+ mSelectedProfile = profileToConnect;
+ launchVPN();
+
+ } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ createListView();
+ }
+ }
+
+ private void createListView() {
+ ListView lv = getListView();
+ //lv.setTextFilterEnabled(true);
+
+ Collection<VpnProfile> vpnlist = mPM.getProfiles();
+
+ Vector<String> vpnnames=new Vector<String>();
+ for (VpnProfile vpnProfile : vpnlist) {
+ vpnnames.add(vpnProfile.mName);
+ }
+
+
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,vpnnames);
+ lv.setAdapter(adapter);
+
+ lv.setOnItemClickListener(this);
+ }
+
+ /**
+ * This function creates a shortcut and returns it to the caller. There are actually two
+ * intents that you will send back.
+ *
+ * The first intent serves as a container for the shortcut and is returned to the launcher by
+ * setResult(). This intent must contain three fields:
+ *
+ * <ul>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with
+ * the shortcut.</li>
+ * <li>{@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a
+ * bitmap, <i>or</i> {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as
+ * a drawable resource.</li>
+ * </ul>
+ *
+ * If you use a simple drawable resource, note that you must wrapper it using
+ * {@link android.content.Intent.ShortcutIconResource}, as shown below. This is required so
+ * that the launcher can access resources that are stored in your application's .apk file. If
+ * you return a bitmap, such as a thumbnail, you can simply put the bitmap into the extras
+ * bundle using {@link android.content.Intent#EXTRA_SHORTCUT_ICON}.
+ *
+ * The shortcut intent can be any intent that you wish the launcher to send, when the user
+ * clicks on the shortcut. Typically this will be {@link android.content.Intent#ACTION_VIEW}
+ * with an appropriate Uri for your content, but any Intent will work here as long as it
+ * triggers the desired action within your Activity.
+ * @param profile
+ */
+ private void setupShortcut(VpnProfile profile) {
+ // First, set up the shortcut intent. For this example, we simply create an intent that
+ // will bring us directly back to this activity. A more typical implementation would use a
+ // data Uri in order to display a more specific result, or a custom action in order to
+ // launch a specific operation.
+
+ Intent shortcutIntent = new Intent(Intent.ACTION_MAIN);
+ shortcutIntent.setClass(this, LaunchVPN.class);
+ shortcutIntent.putExtra(EXTRA_KEY,profile.getUUID().toString());
+
+ // Then, set up the container intent (the response to the caller)
+
+ Intent intent = new Intent();
+ intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, profile.getName());
+ Parcelable iconResource = Intent.ShortcutIconResource.fromContext(
+ this, R.drawable.icon);
+ intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconResource);
+
+ // Now, return the result to the launcher
+
+ setResult(RESULT_OK, intent);
+ }
+
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position,
+ long id) {
+ String profilename = ((TextView) view).getText().toString();
+
+ VpnProfile profile = mPM.getProfileByName(profilename);
+
+ // if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
+ setupShortcut(profile);
+ finish();
+ return;
+ // }
+
+ }
+
+
+
+ private void askForPW(final int type) {
+
+ final EditText entry = new EditText(this);
+ entry.setSingleLine();
+ entry.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
+ entry.setTransformationMethod(new PasswordTransformationMethod());
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Need " + getString(type));
+ dialog.setMessage("Enter the password for profile " + mSelectedProfile.mName);
+ dialog.setView(entry);
+
+ dialog.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String pw = entry.getText().toString();
+ if(type == R.string.password) {
+ mSelectedProfile.mTransientPW = pw;
+ } else {
+ mSelectedProfile.mTransientPCKS12PW = pw;
+ }
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+
+ }
+
+ });
+ dialog.setNegativeButton(android.R.string.cancel,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+ }
+ });
+
+ dialog.create().show();
+
+ }
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+
+ if(requestCode==START_VPN_PROFILE) {
+ if(resultCode == Activity.RESULT_OK) {
+ int needpw = mSelectedProfile.needUserPWInput();
+ if(needpw !=0) {
+ askForPW(needpw);
+ } else {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean showlogwindow = prefs.getBoolean("showlogwindow", false);
+
+ if(!mhideLog && showlogwindow)
+ showLogWindow();
+ new startOpenVpnThread().start();
+ }
+ } else if (resultCode == Activity.RESULT_CANCELED) {
+ // User does not want us to start, so we just vanish (well, now we tell our receiver, then vanish)
+ Bundle resultData = new Bundle();
+ // For now, nothing else is calling, so this "request" string is good enough
+ resultData.putString(EIP.REQUEST_TAG, EIP.ACTION_START_EIP);
+ mReceiver.send(RESULT_CANCELED, resultData);
+ finish();
+ }
+ }
+ }
+ void showLogWindow() {
+
+ Intent startLW = new Intent(getBaseContext(),LogWindow.class);
+ startLW.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ startActivity(startLW);
+
+ }
+
+ void showConfigErrorDialog(int vpnok) {
+ AlertDialog.Builder d = new AlertDialog.Builder(this);
+ d.setTitle(R.string.config_error_found);
+ d.setMessage(vpnok);
+ d.setPositiveButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ finish();
+
+ }
+ });
+ d.show();
+ }
+
+ void launchVPN () {
+ int vpnok = mSelectedProfile.checkProfile(this);
+ if(vpnok!= R.string.no_error_found) {
+ showConfigErrorDialog(vpnok);
+ return;
+ }
+
+ Intent intent = VpnService.prepare(this);
+ // Check if we want to fix /dev/tun
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
+ boolean usecm9fix = prefs.getBoolean("useCM9Fix", false);
+ boolean loadTunModule = prefs.getBoolean("loadTunModule", false);
+
+ if(loadTunModule)
+ execeuteSUcmd("insmod /system/lib/modules/tun.ko");
+
+ if(usecm9fix && !mCmfixed ) {
+ execeuteSUcmd("chown system /dev/tun");
+ }
+
+
+ if (intent != null) {
+ // Start the query
+ try {
+ startActivityForResult(intent, START_VPN_PROFILE);
+ } catch (ActivityNotFoundException ane) {
+ // Shame on you Sony! At least one user reported that
+ // an official Sony Xperia Arc S image triggers this exception
+ OpenVPN.logError(R.string.no_vpn_support_image);
+ showLogWindow();
+ }
+ } else {
+ onActivityResult(START_VPN_PROFILE, Activity.RESULT_OK, null);
+ }
+
+ }
+
+ private void execeuteSUcmd(String command) {
+ ProcessBuilder pb = new ProcessBuilder(new String[] {"su","-c",command});
+ try {
+ Process p = pb.start();
+ int ret = p.waitFor();
+ if(ret ==0)
+ mCmfixed=true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private class startOpenVpnThread extends Thread {
+
+ @Override
+ public void run() {
+ VPNLaunchHelper.startOpenVpn(mSelectedProfile, getBaseContext());
+ // Tell whom-it-may-concern that we started VPN
+ Bundle resultData = new Bundle();
+ // For now, nothing else is calling, so this "request" string is good enough
+ resultData.putString(EIP.REQUEST_TAG, EIP.ACTION_START_EIP);
+ mReceiver.send(RESULT_OK, resultData);
+ finish();
+
+ }
+
+ }
+
+
+}
diff --git a/app/src/main/java/se/leap/openvpn/LogWindow.java b/app/src/main/java/se/leap/openvpn/LogWindow.java
new file mode 100644
index 00000000..b87c4999
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/LogWindow.java
@@ -0,0 +1,340 @@
+package se.leap.openvpn;
+
+import java.util.Vector;
+
+import se.leap.bitmaskclient.R;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.app.ListActivity;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Handler.Callback;
+import android.os.Message;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import se.leap.openvpn.OpenVPN.LogItem;
+import se.leap.openvpn.OpenVPN.LogListener;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class LogWindow extends ListActivity implements StateListener {
+ private static final int START_VPN_CONFIG = 0;
+ private String[] mBconfig=null;
+
+
+ class LogWindowListAdapter implements ListAdapter, LogListener, Callback {
+
+ private static final int MESSAGE_NEWLOG = 0;
+
+ private static final int MESSAGE_CLEARLOG = 1;
+
+ private Vector<String> myEntries=new Vector<String>();
+
+ private Handler mHandler;
+
+ private Vector<DataSetObserver> observers=new Vector<DataSetObserver>();
+
+
+ public LogWindowListAdapter() {
+ initLogBuffer();
+
+ if (mHandler == null) {
+ mHandler = new Handler(this);
+ }
+
+ OpenVPN.addLogListener(this);
+ }
+
+
+
+ private void initLogBuffer() {
+ myEntries.clear();
+ for (LogItem litem : OpenVPN.getlogbuffer()) {
+ myEntries.add(litem.getString(getContext()));
+ }
+ }
+
+ String getLogStr() {
+ String str = "";
+ for(String entry:myEntries) {
+ str+=entry + '\n';
+ }
+ return str;
+ }
+
+
+ private void shareLog() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.putExtra(Intent.EXTRA_TEXT, getLogStr());
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.bitmask_openvpn_log_file));
+ shareIntent.setType("text/plain");
+ startActivity(Intent.createChooser(shareIntent, "Send Logfile"));
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ observers.add(observer);
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ observers.remove(observer);
+ }
+
+ @Override
+ public int getCount() {
+ return myEntries.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return myEntries.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ TextView v;
+ if(convertView==null)
+ v = new TextView(getBaseContext());
+ else
+ v = (TextView) convertView;
+ v.setText(myEntries.get(position));
+ return v;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ return 0;
+ }
+
+ @Override
+ public int getViewTypeCount() {
+ return 1;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return myEntries.isEmpty();
+
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return true;
+ }
+
+ @Override
+ public void newLog(LogItem logmessage) {
+ Message msg = Message.obtain();
+ msg.what=MESSAGE_NEWLOG;
+ Bundle mbundle=new Bundle();
+ mbundle.putString("logmessage", logmessage.getString(getBaseContext()));
+ msg.setData(mbundle);
+ mHandler.sendMessage(msg);
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ // We have been called
+ if(msg.what==MESSAGE_NEWLOG) {
+
+ String logmessage = msg.getData().getString("logmessage");
+ myEntries.add(logmessage);
+
+ for (DataSetObserver observer : observers) {
+ observer.onChanged();
+ }
+ } else if (msg.what == MESSAGE_CLEARLOG) {
+ initLogBuffer();
+ for (DataSetObserver observer : observers) {
+ observer.onInvalidated();
+ }
+ }
+
+ return true;
+ }
+
+ void clearLog() {
+ // Actually is probably called from GUI Thread as result of the user
+ // pressing a button. But better safe than sorry
+ OpenVPN.clearLog();
+ OpenVPN.logMessage(0,"","Log cleared.");
+ mHandler.sendEmptyMessage(MESSAGE_CLEARLOG);
+ }
+ }
+
+
+
+ private LogWindowListAdapter ladapter;
+ private TextView mSpeedView;
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if(item.getItemId()==R.id.clearlog) {
+ ladapter.clearLog();
+ return true;
+ } else if(item.getItemId()==R.id.cancel){
+ Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.title_cancel);
+ builder.setMessage(R.string.cancel_connection_query);
+ builder.setNegativeButton(android.R.string.no, null);
+ builder.setPositiveButton(android.R.string.yes, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ProfileManager.setConntectedVpnProfileDisconnected(getApplicationContext());
+ OpenVpnManagementThread.stopOpenVPN();
+ }
+ });
+
+ builder.show();
+ return true;
+ } else if(item.getItemId()==R.id.info) {
+ if(mBconfig==null)
+ OpenVPN.triggerLogBuilderConfig();
+
+ } else if(item.getItemId()==R.id.send) {
+ ladapter.shareLog();
+ }
+
+ return super.onOptionsItemSelected(item);
+
+ }
+
+ protected Context getContext() {
+ return this;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.logmenu, menu);
+ return true;
+ }
+
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ OpenVPN.addStateListener(this);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == START_VPN_CONFIG && resultCode==RESULT_OK) {
+ String configuredVPN = data.getStringExtra(VpnProfile.EXTRA_PROFILEUUID);
+
+ final VpnProfile profile = ProfileManager.get(configuredVPN);
+ ProfileManager.getInstance(this).saveProfile(this, profile);
+ // Name could be modified, reset List adapter
+
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle(R.string.configuration_changed);
+ dialog.setMessage(R.string.restart_vpn_after_change);
+
+
+ dialog.setPositiveButton(R.string.restart,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(getBaseContext(), LaunchVPN.class);
+ intent.putExtra(LaunchVPN.EXTRA_KEY, profile.getUUIDString());
+ intent.setAction(Intent.ACTION_MAIN);
+ startActivity(intent);
+ }
+
+
+ });
+ dialog.setNegativeButton(R.string.ignore, null);
+ dialog.create().show();
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ OpenVPN.removeStateListener(this);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.logwindow);
+ ListView lv = getListView();
+
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ ClipboardManager clipboard = (ClipboardManager)
+ getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("Log Entry",((TextView) view).getText());
+ clipboard.setPrimaryClip(clip);
+ Toast.makeText(getBaseContext(), R.string.copied_entry, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ });
+
+ ladapter = new LogWindowListAdapter();
+ lv.setAdapter(ladapter);
+
+ mSpeedView = (TextView) findViewById(R.id.speed);
+ }
+
+ @Override
+ public void updateState(final String status,final String logmessage, final int resid) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ String prefix=getString(resid) + ":";
+ if (status.equals("BYTECOUNT") || status.equals("NOPROCESS") )
+ prefix="";
+ mSpeedView.setText(prefix + logmessage);
+ }
+ });
+
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ OpenVPN.removeLogListener(ladapter);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/openvpn/NetworkSateReceiver.java b/app/src/main/java/se/leap/openvpn/NetworkSateReceiver.java
new file mode 100644
index 00000000..777402b4
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/NetworkSateReceiver.java
@@ -0,0 +1,86 @@
+package se.leap.openvpn;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.NetworkInfo.State;
+import android.preference.PreferenceManager;
+import se.leap.bitmaskclient.R;
+
+public class NetworkSateReceiver extends BroadcastReceiver {
+ private int lastNetwork=-1;
+ private OpenVpnManagementThread mManangement;
+
+ private String lastStateMsg=null;
+
+ public NetworkSateReceiver(OpenVpnManagementThread managementThread) {
+ super();
+ mManangement = managementThread;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NetworkInfo networkInfo = getCurrentNetworkInfo(context);
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean sendusr1 = prefs.getBoolean("netchangereconnect", true);
+
+ String netstatestring;
+ if(networkInfo==null)
+ netstatestring = "not connected";
+ else {
+ String subtype = networkInfo.getSubtypeName();
+ if(subtype==null)
+ subtype = "";
+ String extrainfo = networkInfo.getExtraInfo();
+ if(extrainfo==null)
+ extrainfo="";
+
+ /*
+ if(networkInfo.getType()==android.net.ConnectivityManager.TYPE_WIFI) {
+ WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiinfo = wifiMgr.getConnectionInfo();
+ extrainfo+=wifiinfo.getBSSID();
+
+ subtype += wifiinfo.getNetworkId();
+ }*/
+
+
+
+ netstatestring = String.format("%2$s %4$s to %1$s %3$s",networkInfo.getTypeName(),
+ networkInfo.getDetailedState(),extrainfo,subtype );
+ }
+
+
+
+ if(networkInfo!=null && networkInfo.getState() == State.CONNECTED) {
+ int newnet = networkInfo.getType();
+
+ if(sendusr1 && lastNetwork!=newnet)
+ mManangement.reconnect();
+
+ lastNetwork = newnet;
+ } else if (networkInfo==null) {
+ // Not connected, stop openvpn, set last connected network to no network
+ lastNetwork=-1;
+ if(sendusr1)
+ mManangement.signalusr1();
+ }
+
+ if(!netstatestring.equals(lastStateMsg))
+ OpenVPN.logInfo(R.string.netstatus, netstatestring);
+ lastStateMsg=netstatestring;
+
+ }
+
+ private NetworkInfo getCurrentNetworkInfo(Context context) {
+ ConnectivityManager conn = (ConnectivityManager)
+ context.getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ NetworkInfo networkInfo = conn.getActiveNetworkInfo();
+ return networkInfo;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/openvpn/OpenVPN.java b/app/src/main/java/se/leap/openvpn/OpenVPN.java
new file mode 100644
index 00000000..8acdc423
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/OpenVPN.java
@@ -0,0 +1,250 @@
+package se.leap.openvpn;
+
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.R;
+
+
+import android.content.Context;
+import android.os.Build;
+import android.util.Log;
+
+public class OpenVPN {
+
+
+ public static LinkedList<LogItem> logbuffer;
+
+ private static Vector<LogListener> logListener;
+ private static Vector<StateListener> stateListener;
+ private static String[] mBconfig;
+
+ private static String mLaststatemsg;
+
+ private static String mLaststate;
+
+ private static int mLastStateresid=R.string.state_noprocess;
+ public static String TAG="se.leap.openvpn.OpenVPN";
+
+ static {
+ logbuffer = new LinkedList<LogItem>();
+ logListener = new Vector<OpenVPN.LogListener>();
+ stateListener = new Vector<OpenVPN.StateListener>();
+ logInformation();
+ }
+
+ public static class LogItem {
+ public static final int ERROR = 1;
+ public static final int INFO = 2;
+ public static final int VERBOSE = 3;
+
+ private Object [] mArgs = null;
+ private String mMessage = null;
+ private int mRessourceId;
+ // Default log priority
+ int mLevel = INFO;
+
+ public LogItem(int ressourceId, Object[] args) {
+ mRessourceId = ressourceId;
+ mArgs = args;
+ }
+
+
+ public LogItem(int loglevel,int ressourceId, Object[] args) {
+ mRessourceId = ressourceId;
+ mArgs = args;
+ mLevel = loglevel;
+ }
+
+
+ public LogItem(String message) {
+
+ mMessage = message;
+ }
+
+ public LogItem(int loglevel, String msg) {
+ mLevel = loglevel;
+ mMessage = msg;
+ }
+
+
+ public LogItem(int loglevel, int ressourceId) {
+ mRessourceId =ressourceId;
+ mLevel = loglevel;
+ }
+
+
+ public String getString(Context c) {
+ if(mMessage !=null) {
+ return mMessage;
+ } else {
+ if(c!=null) {
+ if(mArgs == null)
+ return c.getString(mRessourceId);
+ else
+ return c.getString(mRessourceId,mArgs);
+ } else {
+ String str = String.format(Locale.ENGLISH,"Log (no context) resid %d", mRessourceId);
+ if(mArgs !=null)
+ for(Object o:mArgs)
+ str += "|" + o.toString();
+ return str;
+ }
+ }
+ }
+ }
+
+ private static final int MAXLOGENTRIES = 500;
+
+
+ public static final String MANAGMENT_PREFIX = "M:";
+
+
+
+
+
+
+ public interface LogListener {
+ void newLog(LogItem logItem);
+ }
+
+ public interface StateListener {
+ void updateState(String state, String logmessage, int localizedResId);
+ }
+
+ synchronized static void logMessage(int level,String prefix, String message)
+ {
+ newlogItem(new LogItem(prefix + message));
+ Log.d("OpenVPN log item", message);
+ }
+
+ synchronized static void clearLog() {
+ logbuffer.clear();
+ logInformation();
+ }
+
+ private static void logInformation() {
+
+ logInfo(R.string.mobile_info,Build.MODEL, Build.BOARD,Build.BRAND,Build.VERSION.SDK_INT);
+ }
+
+ public synchronized static void addLogListener(LogListener ll){
+ logListener.add(ll);
+ }
+
+ public synchronized static void removeLogListener(LogListener ll) {
+ logListener.remove(ll);
+ }
+
+
+ public synchronized static void addStateListener(StateListener sl){
+ stateListener.add(sl);
+ if(mLaststate!=null)
+ sl.updateState(mLaststate, mLaststatemsg, mLastStateresid);
+ }
+
+ private static int getLocalizedState(String state){
+ if (state.equals("CONNECTING"))
+ return R.string.state_connecting;
+ else if (state.equals("WAIT"))
+ return R.string.state_wait;
+ else if (state.equals("AUTH"))
+ return R.string.state_auth;
+ else if (state.equals("GET_CONFIG"))
+ return R.string.state_get_config;
+ else if (state.equals("ASSIGN_IP"))
+ return R.string.state_assign_ip;
+ else if (state.equals("ADD_ROUTES"))
+ return R.string.state_add_routes;
+ else if (state.equals("CONNECTED"))
+ return R.string.state_connected;
+ else if (state.equals("RECONNECTING"))
+ return R.string.state_reconnecting;
+ else if (state.equals("EXITING"))
+ return R.string.state_exiting;
+ else if (state.equals("RESOLVE"))
+ return R.string.state_resolve;
+ else if (state.equals("TCP_CONNECT"))
+ return R.string.state_tcp_connect;
+ else if (state.equals("FATAL"))
+ return R.string.eip_state_not_connected;
+ else
+ return R.string.unknown_state;
+
+ }
+
+ public synchronized static void removeStateListener(StateListener sl) {
+ stateListener.remove(sl);
+ }
+
+
+ synchronized public static LogItem[] getlogbuffer() {
+
+ // The stoned way of java to return an array from a vector
+ // brought to you by eclipse auto complete
+ return (LogItem[]) logbuffer.toArray(new LogItem[logbuffer.size()]);
+
+ }
+ public static void logBuilderConfig(String[] bconfig) {
+ mBconfig = bconfig;
+ }
+ public static void triggerLogBuilderConfig() {
+ if(mBconfig==null) {
+ logMessage(0, "", "No active interface");
+ } else {
+ for (String item : mBconfig) {
+ logMessage(0, "", item);
+ }
+ }
+
+ }
+
+ public static void updateStateString (String state, String msg) {
+ int rid = getLocalizedState(state);
+ updateStateString(state, msg,rid);
+ }
+
+ public synchronized static void updateStateString(String state, String msg, int resid) {
+ if (! "BYTECOUNT".equals(state)) {
+ mLaststate= state;
+ mLaststatemsg = msg;
+ mLastStateresid = resid;
+
+ for (StateListener sl : stateListener) {
+ sl.updateState(state,msg,resid);
+ }
+ }
+ }
+
+ public static void logInfo(String message) {
+ newlogItem(new LogItem(LogItem.INFO, message));
+ }
+
+ public static void logInfo(int ressourceId, Object... args) {
+ newlogItem(new LogItem(LogItem.INFO, ressourceId, args));
+ }
+
+ private static void newlogItem(LogItem logItem) {
+ logbuffer.addLast(logItem);
+ if(logbuffer.size()>MAXLOGENTRIES)
+ logbuffer.removeFirst();
+
+ for (LogListener ll : logListener) {
+ ll.newLog(logItem);
+ }
+ }
+
+ public static void logError(String msg) {
+ newlogItem(new LogItem(LogItem.ERROR, msg));
+
+ }
+
+ public static void logError(int ressourceId) {
+ newlogItem(new LogItem(LogItem.ERROR, ressourceId));
+ }
+ public static void logError(int ressourceId, Object... args) {
+ newlogItem(new LogItem(LogItem.ERROR, ressourceId,args));
+ }
+
+}
diff --git a/app/src/main/java/se/leap/openvpn/OpenVPNThread.java b/app/src/main/java/se/leap/openvpn/OpenVPNThread.java
new file mode 100644
index 00000000..ffd21732
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/OpenVPNThread.java
@@ -0,0 +1,130 @@
+package se.leap.openvpn;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.LinkedList;
+
+import se.leap.bitmaskclient.R;
+
+import android.util.Log;
+import se.leap.openvpn.OpenVPN.LogItem;
+
+public class OpenVPNThread implements Runnable {
+ private static final String DUMP_PATH_STRING = "Dump path: ";
+ private static final String TAG = "OpenVPN";
+ private String[] mArgv;
+ private Process mProcess;
+ private String mNativeDir;
+ private OpenVpnService mService;
+ private String mDumpPath;
+
+ public OpenVPNThread(OpenVpnService service,String[] argv, String nativelibdir)
+ {
+ mArgv = argv;
+ mNativeDir = nativelibdir;
+ mService = service;
+ }
+
+ public void stopProcess() {
+ mProcess.destroy();
+ }
+
+
+
+ @Override
+ public void run() {
+ try {
+ Log.i(TAG, "Starting openvpn");
+ startOpenVPNThreadArgs(mArgv);
+ Log.i(TAG, "Giving up");
+ } catch (Exception e) {
+ e.printStackTrace();
+ Log.e(TAG, "OpenVPNThread Got " + e.toString());
+ } finally {
+ int exitvalue = 0;
+ try {
+ exitvalue = mProcess.waitFor();
+ } catch ( IllegalThreadStateException ite) {
+ OpenVPN.logError("Illegal Thread state: " + ite.getLocalizedMessage());
+ } catch (InterruptedException ie) {
+ OpenVPN.logError("InterruptedException: " + ie.getLocalizedMessage());
+ }
+ if( exitvalue != 0)
+ OpenVPN.logError("Process exited with exit value " + exitvalue);
+
+// OpenVPN.updateStateString("NOPROCESS","No process running.", R.string.state_noprocess); fixes bug #4565
+ if(mDumpPath!=null) {
+ try {
+ BufferedWriter logout = new BufferedWriter(new FileWriter(mDumpPath + ".log"));
+ for(LogItem li :OpenVPN.getlogbuffer()){
+ logout.write(li.getString(null) + "\n");
+ }
+ logout.close();
+ OpenVPN.logError(R.string.minidump_generated);
+ } catch (IOException e) {
+ OpenVPN.logError("Writing minidump log: " +e.getLocalizedMessage());
+ }
+ }
+
+ mService.processDied();
+ Log.i(TAG, "Exiting");
+ }
+ }
+
+ private void startOpenVPNThreadArgs(String[] argv) {
+ LinkedList<String> argvlist = new LinkedList<String>();
+
+ for(String arg:argv)
+ argvlist.add(arg);
+
+ ProcessBuilder pb = new ProcessBuilder(argvlist);
+ // Hack O rama
+
+ // Hack until I find a good way to get the real library path
+ String applibpath = argv[0].replace("/cache/" + VpnProfile.MINIVPN , "/lib");
+
+ String lbpath = pb.environment().get("LD_LIBRARY_PATH");
+ if(lbpath==null)
+ lbpath = applibpath;
+ else
+ lbpath = lbpath + ":" + applibpath;
+
+ if (!applibpath.equals(mNativeDir)) {
+ lbpath = lbpath + ":" + mNativeDir;
+ }
+
+ pb.environment().put("LD_LIBRARY_PATH", lbpath);
+ pb.redirectErrorStream(true);
+ try {
+ mProcess = pb.start();
+ // Close the output, since we don't need it
+ mProcess.getOutputStream().close();
+ InputStream in = mProcess.getInputStream();
+ BufferedReader br = new BufferedReader(new InputStreamReader(in));
+
+ while(true) {
+ String logline = br.readLine();
+ if(logline==null)
+ return;
+
+ if (logline.startsWith(DUMP_PATH_STRING))
+ mDumpPath = logline.substring(DUMP_PATH_STRING.length());
+
+
+ OpenVPN.logMessage(0, "P:", logline);
+ }
+
+
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading from output of OpenVPN process"+ e.getLocalizedMessage());
+ e.printStackTrace();
+ stopProcess();
+ }
+
+
+ }
+}
diff --git a/app/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java b/app/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java
new file mode 100644
index 00000000..27a3db65
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java
@@ -0,0 +1,592 @@
+package se.leap.openvpn;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.util.LinkedList;
+import java.util.Vector;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
+import se.leap.bitmaskclient.R;
+import android.content.SharedPreferences;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+import android.preference.PreferenceManager;
+import android.util.Base64;
+import android.util.Log;
+
+public class OpenVpnManagementThread implements Runnable {
+
+ private static final String TAG = "openvpn";
+ private LocalSocket mSocket;
+ private VpnProfile mProfile;
+ private OpenVpnService mOpenVPNService;
+ private LinkedList<FileDescriptor> mFDList=new LinkedList<FileDescriptor>();
+ private int mBytecountinterval=2;
+ private long mLastIn=0;
+ private long mLastOut=0;
+ private LocalServerSocket mServerSocket;
+ private boolean mReleaseHold=true;
+ private boolean mWaitingForRelease=false;
+ private long mLastHoldRelease=0;
+
+ private static Vector<OpenVpnManagementThread> active=new Vector<OpenVpnManagementThread>();
+
+ static private native void jniclose(int fdint);
+ static private native byte[] rsasign(byte[] input,int pkey) throws InvalidKeyException;
+
+ public OpenVpnManagementThread(VpnProfile profile, LocalServerSocket mgmtsocket, OpenVpnService openVpnService) {
+ mProfile = profile;
+ mServerSocket = mgmtsocket;
+ mOpenVPNService = openVpnService;
+
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(openVpnService);
+ boolean managemeNetworkState = prefs.getBoolean("netchangereconnect", true);
+ if(managemeNetworkState)
+ mReleaseHold=false;
+
+ }
+
+ static {
+ System.loadLibrary("opvpnutil");
+ }
+
+ public void managmentCommand(String cmd) {
+ if(mSocket!=null) {
+ try {
+ mSocket.getOutputStream().write(cmd.getBytes());
+ mSocket.getOutputStream().flush();
+ } catch (IOException e) {
+ // Ignore socket stack traces
+ }
+ }
+ }
+
+
+ @Override
+ public void run() {
+ Log.i(TAG, "Managment Socket Thread started");
+ byte [] buffer =new byte[2048];
+ // mSocket.setSoTimeout(5); // Setting a timeout cannot be that bad
+
+ String pendingInput="";
+ active.add(this);
+
+ try {
+ // Wait for a client to connect
+ mSocket= mServerSocket.accept();
+ InputStream instream = mSocket.getInputStream();
+
+ while(true) {
+ int numbytesread = instream.read(buffer);
+ if(numbytesread==-1)
+ return;
+
+ FileDescriptor[] fds = null;
+ try {
+ fds = mSocket.getAncillaryFileDescriptors();
+ } catch (IOException e) {
+ OpenVPN.logMessage(0, "", "Error reading fds from socket" + e.getLocalizedMessage());
+ e.printStackTrace();
+ }
+ if(fds!=null){
+
+ for (FileDescriptor fd : fds) {
+
+ mFDList.add(fd);
+ }
+ }
+
+ String input = new String(buffer,0,numbytesread,"UTF-8");
+
+ pendingInput += input;
+
+ pendingInput=processInput(pendingInput);
+
+
+
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ active.remove(this);
+ }
+
+ //! Hack O Rama 2000!
+ private void protectFileDescriptor(FileDescriptor fd) {
+ Exception exp=null;
+ try {
+ Method getInt = FileDescriptor.class.getDeclaredMethod("getInt$");
+ int fdint = (Integer) getInt.invoke(fd);
+
+ // You can even get more evil by parsing toString() and extract the int from that :)
+
+ mOpenVPNService.protect(fdint);
+
+ //ParcelFileDescriptor pfd = ParcelFileDescriptor.fromFd(fdint);
+ //pfd.close();
+ jniclose(fdint);
+ return;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (NullPointerException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ exp.printStackTrace();
+ Log.d("Openvpn", "Failed to retrieve fd from socket: " + fd);
+ OpenVPN.logMessage(0, "", "Failed to retrieve fd from socket: " + exp.getLocalizedMessage());
+ }
+ }
+
+ private String processInput(String pendingInput) {
+
+
+ while(pendingInput.contains("\n")) {
+ String[] tokens = pendingInput.split("\\r?\\n", 2);
+ processCommand(tokens[0]);
+ if(tokens.length == 1)
+ // No second part, newline was at the end
+ pendingInput="";
+ else
+ pendingInput=tokens[1];
+ }
+ return pendingInput;
+ }
+
+
+ private void processCommand(String command) {
+ Log.d(TAG, "processCommand: " + command);
+
+ if (command.startsWith(">") && command.contains(":")) {
+ String[] parts = command.split(":",2);
+ String cmd = parts[0].substring(1);
+ String argument = parts[1];
+ if(cmd.equals("INFO")) {
+ // Ignore greeting from mgmt
+ //logStatusMessage(command);
+ }else if (cmd.equals("PASSWORD")) {
+ processPWCommand(argument);
+ } else if (cmd.equals("HOLD")) {
+ handleHold();
+ } else if (cmd.equals("NEED-OK")) {
+ processNeedCommand(argument);
+ } else if (cmd.equals("BYTECOUNT")){
+ processByteCount(argument);
+ } else if (cmd.equals("STATE")) {
+ processState(argument);
+ } else if (cmd.equals("FATAL")){
+ processState(","+cmd+","); //handles FATAL as state
+ } else if (cmd.equals("PROXY")) {
+ processProxyCMD(argument);
+ } else if (cmd.equals("LOG")) {
+ String[] args = argument.split(",",3);
+ // 0 unix time stamp
+ // 1 log level N,I,E etc.
+ // 2 log message
+ OpenVPN.logMessage(0, "", args[2]);
+ } else if (cmd.equals("RSA_SIGN")) {
+ processSignCommand(argument);
+ } else {
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized command" + command);
+ Log.i(TAG, "Got unrecognized command" + command);
+ }
+ } else if (command.startsWith("SUCCESS:")) { //Fixes bug LEAP #4565
+ if (command.equals("SUCCESS: signal SIGINT thrown")){
+ Log.d(TAG, "SUCCESS: signal SIGINT thrown");
+ processState(",EXITING,SIGINT,,");
+ }
+ } else {
+ Log.i(TAG, "Got unrecognized line from managment" + command);
+ OpenVPN.logMessage(0, "MGMT:", "Got unrecognized line from management:" + command);
+ }
+ }
+ private void handleHold() {
+ if(mReleaseHold) {
+ releaseHoldCmd();
+ } else {
+ mWaitingForRelease=true;
+ OpenVPN.updateStateString("NONETWORK", "",R.string.state_nonetwork);
+ }
+ }
+ private void releaseHoldCmd() {
+ if ((System.currentTimeMillis()- mLastHoldRelease) < 5000) {
+ try {
+ Thread.sleep(3000);
+ } catch (InterruptedException e) {}
+
+ }
+ mWaitingForRelease=false;
+ mLastHoldRelease = System.currentTimeMillis();
+ managmentCommand("hold release\n");
+ managmentCommand("bytecount " + mBytecountinterval + "\n");
+ managmentCommand("state on\n");
+ }
+
+ public void releaseHold() {
+ mReleaseHold=true;
+ if(mWaitingForRelease)
+ releaseHoldCmd();
+
+ }
+
+ private void processProxyCMD(String argument) {
+ String[] args = argument.split(",",3);
+ SocketAddress proxyaddr = ProxyDetection.detectProxy(mProfile);
+
+
+ if(args.length >= 2) {
+ String proto = args[1];
+ if(proto.equals("UDP")) {
+ proxyaddr=null;
+ }
+ }
+
+ if(proxyaddr instanceof InetSocketAddress ){
+ InetSocketAddress isa = (InetSocketAddress) proxyaddr;
+
+ OpenVPN.logInfo(R.string.using_proxy, isa.getHostName(),isa.getPort());
+
+ String proxycmd = String.format("proxy HTTP %s %d\n", isa.getHostName(),isa.getPort());
+ managmentCommand(proxycmd);
+ } else {
+ managmentCommand("proxy NONE\n");
+ }
+
+ }
+ private void processState(String argument) {
+ String[] args = argument.split(",",3);
+ String currentstate = args[1];
+ if(args[2].equals(",,")){
+ OpenVPN.updateStateString(currentstate,"");
+ }
+ else if (args[2].endsWith(",,")){ //fixes LEAP Bug #4546
+ args[2] = (String) args[2].subSequence(0, args[2].length()-2);
+ Log.d(TAG, "processState() STATE: "+ currentstate + " msg: " + args[2]);
+ OpenVPN.updateStateString(currentstate,args[2]);
+ }
+ else{
+ OpenVPN.updateStateString(currentstate,args[2]);
+ }
+ }
+
+ private static int repeated_byte_counts = 0;
+ private void processByteCount(String argument) {
+ // >BYTECOUNT:{BYTES_IN},{BYTES_OUT}
+ int comma = argument.indexOf(',');
+ long in = Long.parseLong(argument.substring(0, comma));
+ long out = Long.parseLong(argument.substring(comma+1));
+
+ long diffin = in - mLastIn;
+ long diffout = out - mLastOut;
+ if(diffin == 0 && diffout == 0)
+ repeated_byte_counts++;
+ if(repeated_byte_counts > 3)
+ Log.d("OpenVPN log", "Repeated byte count = " + repeated_byte_counts);
+ mLastIn=in;
+ mLastOut=out;
+
+ String netstat = String.format("In: %8s, %8s/s Out %8s, %8s/s",
+ humanReadableByteCount(in, false),
+ humanReadableByteCount(diffin, false),
+ humanReadableByteCount(out, false),
+ humanReadableByteCount(diffout, false));
+ OpenVPN.updateStateString("BYTECOUNT",netstat);
+
+
+ }
+
+ // From: http://stackoverflow.com/questions/3758606/how-to-convert-byte-size-into-human-readable-format-in-java
+ public static String humanReadableByteCount(long bytes, boolean si) {
+ int unit = si ? 1000 : 1024;
+ if (bytes < unit) return bytes + " B";
+ int exp = (int) (Math.log(bytes) / Math.log(unit));
+ String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
+ return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
+ }
+
+ private void processNeedCommand(String argument) {
+ int p1 =argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+
+ String needed = argument.substring(p1+1, p2);
+ String extra = argument.split(":",2)[1];
+
+ String status = "ok";
+
+
+ if (needed.equals("PROTECTFD")) {
+ FileDescriptor fdtoprotect = mFDList.pollFirst();
+ protectFileDescriptor(fdtoprotect);
+ } else if (needed.equals("DNSSERVER")) {
+ mOpenVPNService.addDNS(extra);
+ }else if (needed.equals("DNSDOMAIN")){
+ mOpenVPNService.setDomain(extra);
+ } else if (needed.equals("ROUTE")) {
+ String[] routeparts = extra.split(" ");
+ mOpenVPNService.addRoute(routeparts[0], routeparts[1]);
+ } else if (needed.equals("ROUTE6")) {
+ mOpenVPNService.addRoutev6(extra);
+ } else if (needed.equals("IFCONFIG")) {
+ String[] ifconfigparts = extra.split(" ");
+ int mtu = Integer.parseInt(ifconfigparts[2]);
+ mOpenVPNService.setLocalIP(ifconfigparts[0], ifconfigparts[1],mtu,ifconfigparts[3]);
+ } else if (needed.equals("IFCONFIG6")) {
+ mOpenVPNService.setLocalIPv6(extra);
+
+ } else if (needed.equals("OPENTUN")) {
+ if(sendTunFD(needed,extra))
+ return;
+ else
+ status="cancel";
+ // This not nice or anything but setFileDescriptors accepts only FilDescriptor class :(
+
+ } else {
+ Log.e(TAG,"Unkown needok command " + argument);
+ return;
+ }
+
+ String cmd = String.format("needok '%s' %s\n", needed, status);
+ managmentCommand(cmd);
+ }
+
+ private boolean sendTunFD (String needed, String extra) {
+ Exception exp = null;
+ if(!extra.equals("tun")) {
+ // We only support tun
+ String errmsg = String.format("Devicetype %s requested, but only tun is possible with the Android API, sorry!",extra);
+ OpenVPN.logMessage(0, "", errmsg );
+
+ return false;
+ }
+ ParcelFileDescriptor pfd = mOpenVPNService.openTun();
+ if(pfd==null)
+ return false;
+
+ Method setInt;
+ int fdint = pfd.getFd();
+ try {
+ setInt = FileDescriptor.class.getDeclaredMethod("setInt$",int.class);
+ FileDescriptor fdtosend = new FileDescriptor();
+
+ setInt.invoke(fdtosend,fdint);
+
+ FileDescriptor[] fds = {fdtosend};
+ mSocket.setFileDescriptorsForSend(fds);
+
+ Log.d("Openvpn", "Sending FD tosocket: " + fdtosend + " " + fdint + " " + pfd);
+ // Trigger a send so we can close the fd on our side of the channel
+ // The API documentation fails to mention that it will not reset the file descriptor to
+ // be send and will happily send the file descriptor on every write ...
+ String cmd = String.format("needok '%s' %s\n", needed, "ok");
+ managmentCommand(cmd);
+
+ // Set the FileDescriptor to null to stop this mad behavior
+ mSocket.setFileDescriptorsForSend(null);
+
+ pfd.close();
+
+ return true;
+ } catch (NoSuchMethodException e) {
+ exp =e;
+ } catch (IllegalArgumentException e) {
+ exp =e;
+ } catch (IllegalAccessException e) {
+ exp =e;
+ } catch (InvocationTargetException e) {
+ exp =e;
+ } catch (IOException e) {
+ exp =e;
+ }
+ if(exp!=null) {
+ OpenVPN.logMessage(0,"", "Could not send fd over socket:" + exp.getLocalizedMessage());
+ exp.printStackTrace();
+ }
+ return false;
+ }
+
+ private void processPWCommand(String argument) {
+ //argument has the form Need 'Private Key' password
+ // or ">PASSWORD:Verification Failed: '%s' ['%s']"
+ String needed;
+
+
+
+ try{
+
+ int p1 = argument.indexOf('\'');
+ int p2 = argument.indexOf('\'',p1+1);
+ needed = argument.substring(p1+1, p2);
+ if (argument.startsWith("Verification Failed")) {
+ proccessPWFailed(needed, argument.substring(p2+1));
+ return;
+ }
+ } catch (StringIndexOutOfBoundsException sioob) {
+ OpenVPN.logMessage(0, "", "Could not parse management Password command: " + argument);
+ return;
+ }
+
+ String pw=null;
+
+ if(needed.equals("Private Key")) {
+ pw = mProfile.getPasswordPrivateKey();
+ } else if (needed.equals("Auth")) {
+ String usercmd = String.format("username '%s' %s\n",
+ needed, VpnProfile.openVpnEscape(mProfile.mUsername));
+ managmentCommand(usercmd);
+ pw = mProfile.getPasswordAuth();
+ }
+ if(pw!=null) {
+ String cmd = String.format("password '%s' %s\n", needed, VpnProfile.openVpnEscape(pw));
+ managmentCommand(cmd);
+ } else {
+ OpenVPN.logMessage(0, OpenVPN.MANAGMENT_PREFIX, String.format("Openvpn requires Authentication type '%s' but no password/key information available", needed));
+ }
+
+ }
+
+
+
+
+ private void proccessPWFailed(String needed, String args) {
+ OpenVPN.updateStateString("AUTH_FAILED", needed + args,R.string.state_auth_failed);
+ }
+ private void logStatusMessage(String command) {
+ OpenVPN.logMessage(0,"MGMT:", command);
+ }
+
+
+ public static boolean stopOpenVPN() {
+ boolean sendCMD=false;
+ for (OpenVpnManagementThread mt: active){
+ mt.managmentCommand("signal SIGINT\n");
+ sendCMD=true;
+ try {
+ if(mt.mSocket !=null)
+ mt.mSocket.close();
+ } catch (IOException e) {
+ // Ignore close error on already closed socket
+ }
+ }
+ return sendCMD;
+ }
+
+ public void signalusr1() {
+ mReleaseHold=false;
+ if(!mWaitingForRelease)
+ managmentCommand("signal SIGUSR1\n");
+ }
+
+ public void reconnect() {
+ signalusr1();
+ releaseHold();
+ }
+
+ private void processSignCommand(String b64data) {
+
+ PrivateKey privkey = mProfile.getKeystoreKey();
+ Exception err =null;
+
+ byte[] data = Base64.decode(b64data, Base64.DEFAULT);
+
+ // The Jelly Bean *evil* Hack
+ // 4.2 implements the RSA/ECB/PKCS1PADDING in the OpenSSLprovider
+ if(Build.VERSION.SDK_INT==16){
+ processSignJellyBeans(privkey,data);
+ return;
+ }
+
+
+ try{
+
+
+ Cipher rsasinger = Cipher.getInstance("RSA/ECB/PKCS1PADDING");
+
+ rsasinger.init(Cipher.ENCRYPT_MODE, privkey);
+
+ byte[] signed_bytes = rsasinger.doFinal(data);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+ } catch (NoSuchAlgorithmException e){
+ err =e;
+ } catch (InvalidKeyException e) {
+ err =e;
+ } catch (NoSuchPaddingException e) {
+ err =e;
+ } catch (IllegalBlockSizeException e) {
+ err =e;
+ } catch (BadPaddingException e) {
+ err =e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+
+
+ private void processSignJellyBeans(PrivateKey privkey, byte[] data) {
+ Exception err =null;
+ try {
+ Method[] allm = privkey.getClass().getSuperclass().getDeclaredMethods();
+ System.out.println(allm);
+ Method getKey = privkey.getClass().getSuperclass().getDeclaredMethod("getOpenSSLKey");
+ getKey.setAccessible(true);
+
+ // Real object type is OpenSSLKey
+ Object opensslkey = getKey.invoke(privkey);
+
+ getKey.setAccessible(false);
+
+ Method getPkeyContext = opensslkey.getClass().getDeclaredMethod("getPkeyContext");
+
+ // integer pointer to EVP_pkey
+ getPkeyContext.setAccessible(true);
+ int pkey = (Integer) getPkeyContext.invoke(opensslkey);
+ getPkeyContext.setAccessible(false);
+
+ byte[] signed_bytes = rsasign(data, pkey);
+ String signed_string = Base64.encodeToString(signed_bytes, Base64.NO_WRAP);
+ managmentCommand("rsa-sig\n");
+ managmentCommand(signed_string);
+ managmentCommand("\nEND\n");
+
+ } catch (NoSuchMethodException e) {
+ err=e;
+ } catch (IllegalArgumentException e) {
+ err=e;
+ } catch (IllegalAccessException e) {
+ err=e;
+ } catch (InvocationTargetException e) {
+ err=e;
+ } catch (InvalidKeyException e) {
+ err=e;
+ }
+ if(err !=null) {
+ OpenVPN.logError(R.string.error_rsa_sign,err.getClass().toString(),err.getLocalizedMessage());
+ }
+
+ }
+}
diff --git a/app/src/main/java/se/leap/openvpn/OpenVpnService.java b/app/src/main/java/se/leap/openvpn/OpenVpnService.java
new file mode 100644
index 00000000..b5c9c798
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/OpenVpnService.java
@@ -0,0 +1,504 @@
+package se.leap.openvpn;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Vector;
+
+import se.leap.bitmaskclient.Dashboard;
+import se.leap.bitmaskclient.R;
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.LocalServerSocket;
+import android.net.LocalSocket;
+import android.net.LocalSocketAddress;
+import android.net.VpnService;
+import android.os.Binder;
+import android.os.Handler.Callback;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.ParcelFileDescriptor;
+import se.leap.openvpn.OpenVPN.StateListener;
+
+public class OpenVpnService extends VpnService implements StateListener, Callback {
+ public static final String START_SERVICE = "se.leap.openvpn.START_SERVICE";
+ public static final String RETRIEVE_SERVICE = "se.leap.openvpn.RETRIEVE_SERVICE";
+
+ private Thread mProcessThread=null;
+
+ private Vector<String> mDnslist=new Vector<String>();
+
+ private VpnProfile mProfile;
+
+ private String mDomain=null;
+
+ private Vector<CIDRIP> mRoutes=new Vector<CIDRIP>();
+ private Vector<String> mRoutesv6=new Vector<String>();
+
+ private CIDRIP mLocalIP=null;
+
+ private OpenVpnManagementThread mSocketManager;
+
+ private Thread mSocketManagerThread;
+ private int mMtu;
+ private String mLocalIPv6=null;
+ private NetworkSateReceiver mNetworkStateReceiver;
+ private NotificationManager mNotificationManager;
+
+ private boolean mDisplayBytecount=false;
+
+ private boolean mStarting=false;
+
+ private long mConnecttime;
+
+
+ private static final int OPENVPN_STATUS = 1;
+
+ public static final int PROTECT_FD = 0;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public OpenVpnService getService() {
+ // Return this instance of LocalService so clients can call public methods
+ return OpenVpnService.this;
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ String action = intent.getAction();
+ if( action !=null && (action.equals(START_SERVICE) || action.equals(RETRIEVE_SERVICE)) )
+ return mBinder;
+ else
+ return super.onBind(intent);
+ }
+
+ @Override
+ public void onRevoke() {
+ OpenVpnManagementThread.stopOpenVPN();
+ endVpnService();
+ }
+
+ // Similar to revoke but do not try to stop process
+ public void processDied() {
+ endVpnService();
+ }
+
+ private void endVpnService() {
+ mProcessThread=null;
+ OpenVPN.logBuilderConfig(null);
+ ProfileManager.setConntectedVpnProfileDisconnected(this);
+ if(!mStarting) {
+ stopSelf();
+ stopForeground(true);
+ }
+ }
+
+ private void showNotification(String state, String msg, String tickerText, boolean lowpriority, long when, boolean persistant) {
+ String ns = Context.NOTIFICATION_SERVICE;
+ mNotificationManager = (NotificationManager) getSystemService(ns);
+ int icon;
+ if (state.equals("NOPROCESS") || state.equals("AUTH_FAILED") || state.equals("NONETWORK") || state.equals("EXITING")){
+ icon = R.drawable.ic_vpn_disconnected;
+ }else{
+ icon = R.drawable.ic_stat_vpn;
+ }
+
+ android.app.Notification.Builder nbuilder = new Notification.Builder(this);
+
+ nbuilder.setContentTitle(getString(R.string.notifcation_title,mProfile.mLocation));
+ nbuilder.setContentText(msg);
+ nbuilder.setOnlyAlertOnce(true);
+ nbuilder.setOngoing(persistant);
+ nbuilder.setContentIntent(getLogPendingIntent());
+ nbuilder.setSmallIcon(icon);
+ if(when !=0)
+ nbuilder.setWhen(when);
+
+
+ // Try to set the priority available since API 16 (Jellybean)
+ jbNotificationExtras(lowpriority, nbuilder);
+ if(tickerText!=null)
+ nbuilder.setTicker(tickerText);
+
+ @SuppressWarnings("deprecation")
+ Notification notification = nbuilder.getNotification();
+
+
+ mNotificationManager.notify(OPENVPN_STATUS, notification);
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private void jbNotificationExtras(boolean lowpriority,
+ android.app.Notification.Builder nbuilder) {
+ try {
+ if(lowpriority) {
+ Method setpriority = nbuilder.getClass().getMethod("setPriority", int.class);
+ // PRIORITY_MIN == -2
+ setpriority.invoke(nbuilder, -2 );
+
+ nbuilder.setUsesChronometer(true);
+ /* PendingIntent cancelconnet=null;
+
+ nbuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel,
+ getString(R.string.cancel_connection),cancelconnet); */
+ }
+
+ //ignore exception
+ } catch (NoSuchMethodException nsm) {
+ } catch (IllegalArgumentException e) {
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) {
+ }
+
+ }
+
+ PendingIntent getLogPendingIntent() {
+ // Let the configure Button show the Dashboard
+ Intent intent = new Intent(Dashboard.getAppContext(),Dashboard.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent startLW = PendingIntent.getActivity(getApplicationContext(), 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ return startLW;
+
+ }
+
+
+ private LocalServerSocket openManagmentInterface(int tries) {
+ // Could take a while to open connection
+ String socketname = (getCacheDir().getAbsolutePath() + "/" + "mgmtsocket");
+ LocalSocket sock = new LocalSocket();
+
+ while(tries > 0 && !sock.isConnected()) {
+ try {
+ sock.bind(new LocalSocketAddress(socketname,
+ LocalSocketAddress.Namespace.FILESYSTEM));
+ } catch (IOException e) {
+ // wait 300 ms before retrying
+ try { Thread.sleep(300);
+ } catch (InterruptedException e1) {}
+
+ }
+ tries--;
+ }
+
+ try {
+ LocalServerSocket lss = new LocalServerSocket(sock.getFileDescriptor());
+ return lss;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return null;
+
+
+ }
+
+ void registerNetworkStateReceiver() {
+ // Registers BroadcastReceiver to track network connection changes.
+ IntentFilter filter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
+ mNetworkStateReceiver = new NetworkSateReceiver(mSocketManager);
+ this.registerReceiver(mNetworkStateReceiver, filter);
+ }
+
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if( intent != null && intent.getAction() !=null &&
+ (intent.getAction().equals(START_SERVICE) || intent.getAction().equals(RETRIEVE_SERVICE)) )
+ return START_NOT_STICKY;
+
+
+ // Extract information from the intent.
+ String prefix = getPackageName();
+ String[] argv = intent.getStringArrayExtra(prefix + ".ARGV");
+ String nativelibdir = intent.getStringExtra(prefix + ".nativelib");
+ String profileUUID = intent.getStringExtra(prefix + ".profileUUID");
+
+ mProfile = ProfileManager.get(profileUUID);
+
+ //showNotification("Starting VPN " + mProfile.mName,"Starting VPN " + mProfile.mName, false,0);
+
+
+ OpenVPN.addStateListener(this);
+
+ // Set a flag that we are starting a new VPN
+ mStarting=true;
+ // Stop the previous session by interrupting the thread.
+ if(OpenVpnManagementThread.stopOpenVPN()){
+ // an old was asked to exit, wait 2s
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+
+ if (mProcessThread!=null) {
+ mProcessThread.interrupt();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ }
+ }
+ // An old running VPN should now be exited
+ mStarting=false;
+
+
+ // Open the Management Interface
+ LocalServerSocket mgmtsocket = openManagmentInterface(8);
+
+ if(mgmtsocket!=null) {
+ // start a Thread that handles incoming messages of the managment socket
+ mSocketManager = new OpenVpnManagementThread(mProfile,mgmtsocket,this);
+ mSocketManagerThread = new Thread(mSocketManager,"OpenVPNMgmtThread");
+ mSocketManagerThread.start();
+ OpenVPN.logInfo("started Socket Thread");
+ registerNetworkStateReceiver();
+ }
+
+
+ // Start a new session by creating a new thread.
+ OpenVPNThread processThread = new OpenVPNThread(this, argv,nativelibdir);
+
+ mProcessThread = new Thread(processThread, "OpenVPNProcessThread");
+ mProcessThread.start();
+
+ ProfileManager.setConnectedVpnProfile(this, mProfile);
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mProcessThread != null) {
+ mSocketManager.managmentCommand("signal SIGINT\n");
+
+ mProcessThread.interrupt();
+ }
+ if (mNetworkStateReceiver!= null) {
+ this.unregisterReceiver(mNetworkStateReceiver);
+ }
+
+ }
+
+
+
+ public ParcelFileDescriptor openTun() {
+ Builder builder = new Builder();
+
+ if(mLocalIP==null && mLocalIPv6==null) {
+ OpenVPN.logMessage(0, "", getString(R.string.opentun_no_ipaddr));
+ return null;
+ }
+
+ if(mLocalIP!=null) {
+ builder.addAddress(mLocalIP.mIp, mLocalIP.len);
+ }
+
+ if(mLocalIPv6!=null) {
+ String[] ipv6parts = mLocalIPv6.split("/");
+ builder.addAddress(ipv6parts[0],Integer.parseInt(ipv6parts[1]));
+ }
+
+
+ for (String dns : mDnslist ) {
+ try {
+ builder.addDnsServer(dns);
+ } catch (IllegalArgumentException iae) {
+ OpenVPN.logError(R.string.dns_add_error, dns,iae.getLocalizedMessage());
+ }
+ }
+
+
+ builder.setMtu(mMtu);
+
+
+ for (CIDRIP route:mRoutes) {
+ try {
+ builder.addRoute(route.mIp, route.len);
+ } catch (IllegalArgumentException ia) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + route + " " + ia.getLocalizedMessage());
+ }
+ }
+
+ for(String v6route:mRoutesv6) {
+ try {
+ String[] v6parts = v6route.split("/");
+ builder.addRoute(v6parts[0],Integer.parseInt(v6parts[1]));
+ } catch (IllegalArgumentException ia) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_rejected) + v6route + " " + ia.getLocalizedMessage());
+ }
+ }
+
+ if(mDomain!=null)
+ builder.addSearchDomain(mDomain);
+
+ String bconfig[] = new String[6];
+
+ bconfig[0]= getString(R.string.last_openvpn_tun_config);
+ bconfig[1] = getString(R.string.local_ip_info,mLocalIP.mIp,mLocalIP.len,mLocalIPv6, mMtu);
+ bconfig[2] = getString(R.string.dns_server_info, joinString(mDnslist));
+ bconfig[3] = getString(R.string.dns_domain_info, mDomain);
+ bconfig[4] = getString(R.string.routes_info, joinString(mRoutes));
+ bconfig[5] = getString(R.string.routes_info6, joinString(mRoutesv6));
+
+ String session = mProfile.mLocation;
+ /* we don't want the IP address in the notification bar
+ if(mLocalIP!=null && mLocalIPv6!=null)
+ session = getString(R.string.session_ipv6string,session, mLocalIP, mLocalIPv6);
+ else if (mLocalIP !=null)
+ session= getString(R.string.session_ipv4string, session, mLocalIP);
+ */
+ builder.setSession(session);
+
+
+ OpenVPN.logBuilderConfig(bconfig);
+
+ // No DNS Server, log a warning
+ if(mDnslist.size()==0)
+ OpenVPN.logInfo(R.string.warn_no_dns);
+
+ // Reset information
+ mDnslist.clear();
+ mRoutes.clear();
+ mRoutesv6.clear();
+ mLocalIP=null;
+ mLocalIPv6=null;
+ mDomain=null;
+
+ builder.setConfigureIntent(getLogPendingIntent());
+
+ try {
+ ParcelFileDescriptor pfd = builder.establish();
+ return pfd;
+ } catch (Exception e) {
+ OpenVPN.logMessage(0, "", getString(R.string.tun_open_error));
+ OpenVPN.logMessage(0, "", getString(R.string.error) + e.getLocalizedMessage());
+ OpenVPN.logMessage(0, "", getString(R.string.tun_error_helpful));
+ return null;
+ }
+
+ }
+
+
+ // Ugly, but java has no such method
+ private <T> String joinString(Vector<T> vec) {
+ String ret = "";
+ if(vec.size() > 0){
+ ret = vec.get(0).toString();
+ for(int i=1;i < vec.size();i++) {
+ ret = ret + ", " + vec.get(i).toString();
+ }
+ }
+ return ret;
+ }
+
+
+
+
+
+
+ public void addDNS(String dns) {
+ mDnslist.add(dns);
+ }
+
+
+ public void setDomain(String domain) {
+ if(mDomain==null) {
+ mDomain=domain;
+ }
+ }
+
+
+ public void addRoute(String dest, String mask) {
+ CIDRIP route = new CIDRIP(dest, mask);
+ if(route.len == 32 && !mask.equals("255.255.255.255")) {
+ OpenVPN.logMessage(0, "", getString(R.string.route_not_cidr,dest,mask));
+ }
+
+ if(route.normalise())
+ OpenVPN.logMessage(0, "", getString(R.string.route_not_netip,dest,route.len,route.mIp));
+
+ mRoutes.add(route);
+ }
+
+ public void addRoutev6(String extra) {
+ mRoutesv6.add(extra);
+ }
+
+
+ public void setLocalIP(String local, String netmask,int mtu, String mode) {
+ mLocalIP = new CIDRIP(local, netmask);
+ mMtu = mtu;
+
+ if(mLocalIP.len == 32 && !netmask.equals("255.255.255.255")) {
+ // get the netmask as IP
+ long netint = CIDRIP.getInt(netmask);
+ if(Math.abs(netint - mLocalIP.getInt()) ==1) {
+ if(mode.equals("net30"))
+ mLocalIP.len=30;
+ else
+ mLocalIP.len=31;
+ } else {
+ OpenVPN.logMessage(0, "", getString(R.string.ip_not_cidr, local,netmask,mode));
+ }
+ }
+ }
+
+ public void setLocalIPv6(String ipv6addr) {
+ mLocalIPv6 = ipv6addr;
+ }
+
+ public boolean isRunning() {
+ if (mStarting == true || mProcessThread != null)
+ return true;
+ else
+ return false;
+ }
+
+ @Override
+ public void updateState(String state,String logmessage, int resid) {
+ // If the process is not running, ignore any state,
+ // Notification should be invisible in this state
+ if(mProcessThread==null)
+ return;
+ if("CONNECTED".equals(state)) {
+ mNotificationManager.cancel(OPENVPN_STATUS);
+ } else if(!"BYTECOUNT".equals(state)) {
+
+ // Other notifications are shown,
+ // This also mean we are no longer connected, ignore bytecount messages until next
+ // CONNECTED
+ String ticker = getString(resid);
+ boolean persist = false;
+ if (("NOPROCESS".equals(state) ) || ("EXITING").equals(state)){
+ showNotification(state, getString(R.string.eip_state_not_connected), ticker, false, 0, persist);
+ }
+ else if (state.equals("GET_CONFIG") || state.equals("ASSIGN_IP")){ //don't show them in the notification message
+ }
+ else{
+ persist = true;
+ showNotification(state, getString(resid) +" " + logmessage,ticker,false,0,persist);
+ }
+ }
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ Runnable r = msg.getCallback();
+ if(r!=null){
+ r.run();
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/openvpn/ProfileManager.java b/app/src/main/java/se/leap/openvpn/ProfileManager.java
new file mode 100644
index 00000000..b9eb3ab6
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/ProfileManager.java
@@ -0,0 +1,220 @@
+package se.leap.openvpn;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.preference.PreferenceManager;
+
+public class ProfileManager {
+ private static final String PREFS_NAME = "VPNList";
+
+
+
+ private static final String ONBOOTPROFILE = "onBootProfile";
+
+
+
+ private static ProfileManager instance;
+
+
+
+ private static VpnProfile mLastConnectedVpn=null;
+ private HashMap<String,VpnProfile> profiles=new HashMap<String, VpnProfile>();
+ private static VpnProfile tmpprofile=null;
+
+
+ public static VpnProfile get(String key) {
+ if (tmpprofile!=null && tmpprofile.getUUIDString().equals(key))
+ return tmpprofile;
+
+ if(instance==null)
+ return null;
+ return instance.profiles.get(key);
+
+ }
+
+
+
+ private ProfileManager() { }
+
+ private static void checkInstance(Context context) {
+ if(instance == null) {
+ instance = new ProfileManager();
+ instance.loadVPNList(context);
+ }
+ }
+
+ synchronized public static ProfileManager getInstance(Context context) {
+ checkInstance(context);
+ return instance;
+ }
+
+ public static void setConntectedVpnProfileDisconnected(Context c) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+ Editor prefsedit = prefs.edit();
+ prefsedit.putString(ONBOOTPROFILE, null);
+ prefsedit.apply();
+
+ }
+
+ public static void setConnectedVpnProfile(Context c, VpnProfile connectedrofile) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+ Editor prefsedit = prefs.edit();
+
+ prefsedit.putString(ONBOOTPROFILE, connectedrofile.getUUIDString());
+ prefsedit.apply();
+ mLastConnectedVpn=connectedrofile;
+
+ }
+
+ public static VpnProfile getOnBootProfile(Context c) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c);
+
+ boolean useStartOnBoot = prefs.getBoolean("restartvpnonboot", false);
+
+
+ String mBootProfileUUID = prefs.getString(ONBOOTPROFILE,null);
+ if(useStartOnBoot && mBootProfileUUID!=null)
+ return get(c, mBootProfileUUID);
+ else
+ return null;
+ }
+
+
+
+
+ public Collection<VpnProfile> getProfiles() {
+ return profiles.values();
+ }
+
+ public VpnProfile getProfileByName(String name) {
+ for (VpnProfile vpnp : profiles.values()) {
+ if(vpnp.getName().equals(name)) {
+ return vpnp;
+ }
+ }
+ return null;
+ }
+
+ public void saveProfileList(Context context) {
+ SharedPreferences sharedprefs = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE);
+ Editor editor = sharedprefs.edit();
+ editor.putStringSet("vpnlist", profiles.keySet());
+
+ // For reasing I do not understand at all
+ // Android saves my prefs file only one time
+ // if I remove the debug code below :(
+ int counter = sharedprefs.getInt("counter", 0);
+ editor.putInt("counter", counter+1);
+ editor.apply();
+
+ }
+
+ public void addProfile(VpnProfile profile) {
+ profiles.put(profile.getUUID().toString(),profile);
+
+ }
+
+ public static void setTemporaryProfile(VpnProfile tmp) {
+ ProfileManager.tmpprofile = tmp;
+ }
+
+
+ public void saveProfile(Context context,VpnProfile profile) {
+ // First let basic settings save its state
+
+ ObjectOutputStream vpnfile;
+ try {
+ vpnfile = new ObjectOutputStream(context.openFileOutput((profile.getUUID().toString() + ".vp"),Activity.MODE_PRIVATE));
+
+ vpnfile.writeObject(profile);
+ vpnfile.flush();
+ vpnfile.close();
+ } catch (FileNotFoundException e) {
+
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ } catch (IOException e) {
+
+ e.printStackTrace();
+ throw new RuntimeException(e);
+
+ }
+ }
+
+
+ private void loadVPNList(Context context) {
+ profiles = new HashMap<String, VpnProfile>();
+ SharedPreferences listpref = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE);
+ Set<String> vlist = listpref.getStringSet("vpnlist", null);
+ Exception exp =null;
+ if(vlist==null){
+ vlist = new HashSet<String>();
+ }
+
+ for (String vpnentry : vlist) {
+ try {
+ ObjectInputStream vpnfile = new ObjectInputStream(context.openFileInput(vpnentry + ".vp"));
+ VpnProfile vp = ((VpnProfile) vpnfile.readObject());
+
+ // Sanity check
+ if(vp==null || vp.mName==null || vp.getUUID()==null)
+ continue;
+
+ profiles.put(vp.getUUID().toString(), vp);
+
+ } catch (StreamCorruptedException e) {
+ exp=e;
+ } catch (FileNotFoundException e) {
+ exp=e;
+ } catch (IOException e) {
+ exp=e;
+ } catch (ClassNotFoundException e) {
+ exp=e;
+ }
+ if(exp!=null) {
+ exp.printStackTrace();
+ }
+ }
+ }
+
+ public int getNumberOfProfiles() {
+ return profiles.size();
+ }
+
+
+
+ public void removeProfile(Context context,VpnProfile profile) {
+ String vpnentry = profile.getUUID().toString();
+ profiles.remove(vpnentry);
+ saveProfileList(context);
+ context.deleteFile(vpnentry + ".vp");
+ if(mLastConnectedVpn==profile)
+ mLastConnectedVpn=null;
+
+ }
+
+
+
+ public static VpnProfile get(Context context, String profileUUID) {
+ checkInstance(context);
+ return get(profileUUID);
+ }
+
+
+
+ public static VpnProfile getLastConnectedVpn() {
+ return mLastConnectedVpn;
+ }
+
+}
diff --git a/app/src/main/java/se/leap/openvpn/ProxyDetection.java b/app/src/main/java/se/leap/openvpn/ProxyDetection.java
new file mode 100644
index 00000000..c7b3d196
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/ProxyDetection.java
@@ -0,0 +1,54 @@
+package se.leap.openvpn;
+
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.List;
+
+import se.leap.bitmaskclient.R;
+
+public class ProxyDetection {
+ static SocketAddress detectProxy(VpnProfile vp) {
+ // Construct a new url with https as protocol
+ try {
+ URL url = new URL(String.format("https://%s:%s",vp.mServerName,vp.mServerPort));
+ Proxy proxy = getFirstProxy(url);
+
+ if(proxy==null)
+ return null;
+ SocketAddress addr = proxy.address();
+ if (addr instanceof InetSocketAddress) {
+ return addr;
+ }
+
+ } catch (MalformedURLException e) {
+ OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage());
+ } catch (URISyntaxException e) {
+ OpenVPN.logError(R.string.getproxy_error,e.getLocalizedMessage());
+ }
+ return null;
+ }
+
+ static Proxy getFirstProxy(URL url) throws URISyntaxException {
+ System.setProperty("java.net.useSystemProxies", "true");
+
+ List<Proxy> proxylist = ProxySelector.getDefault().select(url.toURI());
+
+
+ if (proxylist != null) {
+ for (Proxy proxy: proxylist) {
+ SocketAddress addr = proxy.address();
+
+ if (addr != null) {
+ return proxy;
+ }
+ }
+
+ }
+ return null;
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/openvpn/VPNLaunchHelper.java b/app/src/main/java/se/leap/openvpn/VPNLaunchHelper.java
new file mode 100644
index 00000000..418cf7e9
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/VPNLaunchHelper.java
@@ -0,0 +1,76 @@
+package se.leap.openvpn;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import se.leap.bitmaskclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+public class VPNLaunchHelper {
+ static private boolean writeMiniVPN(Context context) {
+ File mvpnout = new File(context.getCacheDir(),VpnProfile.MINIVPN);
+ if (mvpnout.exists() && mvpnout.canExecute())
+ return true;
+
+ IOException e2 = null;
+
+ try {
+ InputStream mvpn;
+
+ try {
+ mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI);
+ }
+ catch (IOException errabi) {
+ OpenVPN.logInfo("Failed getting assets for archicture " + Build.CPU_ABI);
+ e2=errabi;
+ mvpn = context.getAssets().open("minivpn." + Build.CPU_ABI2);
+
+ }
+
+
+ FileOutputStream fout = new FileOutputStream(mvpnout);
+
+ byte buf[]= new byte[4096];
+
+ int lenread = mvpn.read(buf);
+ while(lenread> 0) {
+ fout.write(buf, 0, lenread);
+ lenread = mvpn.read(buf);
+ }
+ fout.close();
+
+ if(!mvpnout.setExecutable(true)) {
+ OpenVPN.logMessage(0, "","Failed to set minivpn executable");
+ return false;
+ }
+
+
+ return true;
+ } catch (IOException e) {
+ if(e2!=null)
+ OpenVPN.logMessage(0, "",e2.getLocalizedMessage());
+ OpenVPN.logMessage(0, "",e.getLocalizedMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+
+ public static void startOpenVpn(VpnProfile startprofile, Context context) {
+ if(!writeMiniVPN(context)) {
+ OpenVPN.logMessage(0, "", "Error writing minivpn binary");
+ return;
+ }
+ OpenVPN.logMessage(0, "", context.getString(R.string.building_configration));
+
+ Intent startVPN = startprofile.prepareIntent(context);
+ if(startVPN!=null)
+ context.startService(startVPN);
+
+ }
+}
diff --git a/app/src/main/java/se/leap/openvpn/VpnProfile.java b/app/src/main/java/se/leap/openvpn/VpnProfile.java
new file mode 100644
index 00000000..481819ad
--- /dev/null
+++ b/app/src/main/java/se/leap/openvpn/VpnProfile.java
@@ -0,0 +1,758 @@
+package se.leap.openvpn;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Serializable;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.UUID;
+import java.util.Vector;
+
+import org.spongycastle.util.io.pem.PemObject;
+import org.spongycastle.util.io.pem.PemWriter;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.Dashboard;
+import se.leap.bitmaskclient.EIP;
+import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.R;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.preference.PreferenceManager;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+
+public class VpnProfile implements Serializable{
+ // Parcable
+ /**
+ *
+ */
+ private static final long serialVersionUID = 7085688938959334563L;
+ static final int TYPE_CERTIFICATES=0;
+ static final int TYPE_PKCS12=1;
+ static final int TYPE_KEYSTORE=2;
+ public static final int TYPE_USERPASS = 3;
+ public static final int TYPE_STATICKEYS = 4;
+ public static final int TYPE_USERPASS_CERTIFICATES = 5;
+ public static final int TYPE_USERPASS_PKCS12 = 6;
+ public static final int TYPE_USERPASS_KEYSTORE = 7;
+
+ // Don't change this, not all parts of the program use this constant
+ public static final String EXTRA_PROFILEUUID = "se.leap.bitmaskclient.profileUUID"; // TODO this feels wrong. See Issue #1494
+ public static final String INLINE_TAG = "[[INLINE]]";
+ private static final String OVPNCONFIGFILE = "android.conf";
+
+ protected transient String mTransientPW=null;
+ protected transient String mTransientPCKS12PW=null;
+ private transient PrivateKey mPrivateKey;
+ protected boolean profileDleted=false;
+
+
+ public static String DEFAULT_DNS1="131.234.137.23";
+ public static String DEFAULT_DNS2="131.234.137.24";
+
+ // Public attributes, since I got mad with getter/setter
+ // set members to default values
+ private UUID mUuid;
+ public int mAuthenticationType = TYPE_CERTIFICATES ;
+ public String mName;
+ public String mLocation;
+ public String mAlias;
+ public String mClientCertFilename;
+ public String mTLSAuthDirection="";
+ public String mTLSAuthFilename;
+ public String mClientKeyFilename;
+ public String mCaFilename;
+ public boolean mUseLzo=true;
+ public String mServerPort= "1194" ;
+ public boolean mUseUdp = true;
+ public String mPKCS12Filename;
+ public String mPKCS12Password;
+ public boolean mUseTLSAuth = false;
+ public String mServerName = "openvpn.blinkt.de" ;
+ public String mDNS1=DEFAULT_DNS1;
+ public String mDNS2=DEFAULT_DNS2;
+ public String mIPv4Address;
+ public String mIPv6Address;
+ public boolean mOverrideDNS=false;
+ public String mSearchDomain="blinkt.de";
+ public boolean mUseDefaultRoute=true;
+ public boolean mUsePull=true;
+ public String mCustomRoutes;
+ public boolean mCheckRemoteCN=false;
+ public boolean mExpectTLSCert=true;
+ public String mRemoteCN="";
+ public String mPassword="";
+ public String mUsername="";
+ public boolean mRoutenopull=false;
+ public boolean mUseRandomHostname=false;
+ public boolean mUseFloat=false;
+ public boolean mUseCustomConfig=false;
+ public String mCustomConfigOptions="";
+ public String mVerb="1";
+ public String mCipher="";
+ public boolean mNobind=false;
+ public boolean mUseDefaultRoutev6=true;
+ public String mCustomRoutesv6="";
+ public String mKeyPassword="";
+ public boolean mPersistTun = false;
+ public String mConnectRetryMax="5";
+ public String mConnectRetry="10";
+ public boolean mUserEditable=true;
+
+ static final String MINIVPN = "miniopenvpn";
+
+
+
+
+
+
+ public void clearDefaults() {
+ mServerName="unkown";
+ mUsePull=false;
+ mUseLzo=false;
+ mUseDefaultRoute=false;
+ mUseDefaultRoutev6=false;
+ mExpectTLSCert=false;
+ mPersistTun = false;
+ }
+
+
+ public static String openVpnEscape(String unescaped) {
+ if(unescaped==null)
+ return null;
+ String escapedString = unescaped.replace("\\", "\\\\");
+ escapedString = escapedString.replace("\"","\\\"");
+ escapedString = escapedString.replace("\n","\\n");
+
+ if (escapedString.equals(unescaped) && !escapedString.contains(" ") && !escapedString.contains("#"))
+ return unescaped;
+ else
+ return '"' + escapedString + '"';
+ }
+
+
+ static final String OVPNCONFIGCA = "android-ca.pem";
+ static final String OVPNCONFIGUSERCERT = "android-user.pem";
+
+
+ public VpnProfile(String name) {
+ mUuid = UUID.randomUUID();
+ mName = name;
+ }
+
+ public UUID getUUID() {
+ return mUuid;
+
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+
+ public String getConfigFile(Context context)
+ {
+
+ File cacheDir= context.getCacheDir();
+ String cfg="";
+
+ // Enable managment interface
+ cfg += "# Enables connection to GUI\n";
+ cfg += "management ";
+
+ cfg +=cacheDir.getAbsolutePath() + "/" + "mgmtsocket";
+ cfg += " unix\n";
+ cfg += "management-client\n";
+ // Not needed, see updated man page in 2.3
+ //cfg += "management-signal\n";
+ cfg += "management-query-passwords\n";
+ cfg += "management-hold\n\n";
+
+ /* tmp-dir patched out :)
+ cfg+="# /tmp does not exist on Android\n";
+ cfg+="tmp-dir ";
+ cfg+=cacheDir.getAbsolutePath();
+ cfg+="\n\n"; */
+
+ cfg+="# Log window is better readable this way\n";
+ cfg+="suppress-timestamps\n";
+
+
+
+ boolean useTLSClient = (mAuthenticationType != TYPE_STATICKEYS);
+
+ if(useTLSClient && mUsePull)
+ cfg+="client\n";
+ else if (mUsePull)
+ cfg+="pull\n";
+ else if(useTLSClient)
+ cfg+="tls-client\n";
+
+
+ cfg+="verb " + mVerb + "\n";
+
+ if(mConnectRetryMax ==null) {
+ mConnectRetryMax="5";
+ }
+
+ if(!mConnectRetryMax.equals("-1"))
+ cfg+="connect-retry-max " + mConnectRetryMax+ "\n";
+
+ if(mConnectRetry==null)
+ mConnectRetry="10";
+
+
+ cfg+="connect-retry " + mConnectRetry + "\n";
+
+ cfg+="resolv-retry 60\n";
+
+
+
+ // We cannot use anything else than tun
+ cfg+="dev tun\n";
+
+ // Server Address
+ cfg+="remote ";
+ cfg+=mServerName;
+ cfg+=" ";
+ cfg+=mServerPort;
+ if(mUseUdp)
+ cfg+=" udp\n";
+ else
+ cfg+=" tcp-client\n";
+
+
+
+
+ switch(mAuthenticationType) {
+ case VpnProfile.TYPE_USERPASS_CERTIFICATES:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_CERTIFICATES:
+ /*// Ca
+ cfg+=insertFileData("ca",mCaFilename);
+
+ // Client Cert + Key
+ cfg+=insertFileData("key",mClientKeyFilename);
+ cfg+=insertFileData("cert",mClientCertFilename);
+*/
+ // FIXME This is all we need...The whole switch statement can go...
+ SharedPreferences preferences = context.getSharedPreferences(Dashboard.SHARED_PREFERENCES, context.MODE_PRIVATE);
+ cfg+="<ca>\n"+preferences.getString(Provider.CA_CERT, "")+"\n</ca>\n";
+ cfg+="<key>\n"+preferences.getString(EIP.PRIVATE_KEY, "")+"\n</key>\n";
+ cfg+="<cert>\n"+preferences.getString(EIP.CERTIFICATE, "")+"\n</cert>\n";
+
+ break;
+ case VpnProfile.TYPE_USERPASS_PKCS12:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_PKCS12:
+ cfg+=insertFileData("pkcs12",mPKCS12Filename);
+ break;
+
+ case VpnProfile.TYPE_USERPASS_KEYSTORE:
+ cfg+="auth-user-pass\n";
+ case VpnProfile.TYPE_KEYSTORE:
+ cfg+="ca " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGCA + "\n";
+ cfg+="cert " + cacheDir.getAbsolutePath() + "/" + OVPNCONFIGUSERCERT + "\n";
+ cfg+="management-external-key\n";
+
+ break;
+ case VpnProfile.TYPE_USERPASS:
+ cfg+="auth-user-pass\n";
+ cfg+=insertFileData("ca",mCaFilename);
+ }
+
+ if(mUseLzo) {
+ cfg+="comp-lzo\n";
+ }
+
+ if(mUseTLSAuth) {
+ if(mAuthenticationType==TYPE_STATICKEYS)
+ cfg+=insertFileData("secret",mTLSAuthFilename);
+ else
+ cfg+=insertFileData("tls-auth",mTLSAuthFilename);
+
+ if(nonNull(mTLSAuthDirection)) {
+ cfg+= "key-direction ";
+ cfg+= mTLSAuthDirection;
+ cfg+="\n";
+ }
+
+ }
+
+ if(!mUsePull ) {
+ if(nonNull(mIPv4Address))
+ cfg +="ifconfig " + cidrToIPAndNetmask(mIPv4Address) + "\n";
+
+ if(nonNull(mIPv6Address))
+ cfg +="ifconfig-ipv6 " + mIPv6Address + "\n";
+ }
+
+ if(mUsePull && mRoutenopull)
+ cfg += "route-nopull\n";
+
+ String routes = "";
+ int numroutes=0;
+ if(mUseDefaultRoute)
+ routes += "route 0.0.0.0 0.0.0.0\n";
+ else
+ for(String route:getCustomRoutes()) {
+ routes += "route " + route + "\n";
+ numroutes++;
+ }
+
+
+ if(mUseDefaultRoutev6)
+ cfg += "route-ipv6 ::/0\n";
+ else
+ for(String route:getCustomRoutesv6()) {
+ routes += "route-ipv6 " + route + "\n";
+ numroutes++;
+ }
+
+ // Round number to next 100
+ if(numroutes> 90) {
+ numroutes = ((numroutes / 100)+1) * 100;
+ cfg+="# Alot of routes are set, increase max-routes\n";
+ cfg+="max-routes " + numroutes + "\n";
+ }
+ cfg+=routes;
+
+ if(mOverrideDNS || !mUsePull) {
+ if(nonNull(mDNS1))
+ cfg+="dhcp-option DNS " + mDNS1 + "\n";
+ if(nonNull(mDNS2))
+ cfg+="dhcp-option DNS " + mDNS2 + "\n";
+ if(nonNull(mSearchDomain))
+ cfg+="dhcp-option DOMAIN " + mSearchDomain + "\n";
+
+ }
+
+ if(mNobind)
+ cfg+="nobind\n";
+
+
+
+ // Authentication
+ if(mCheckRemoteCN) {
+ if(mRemoteCN == null || mRemoteCN.equals("") )
+ cfg+="tls-remote " + mServerName + "\n";
+ else
+ cfg += "tls-remote " + openVpnEscape(mRemoteCN) + "\n";
+ }
+ if(mExpectTLSCert)
+ cfg += "remote-cert-tls server\n";
+
+
+ if(nonNull(mCipher)){
+ cfg += "cipher " + mCipher + "\n";
+ }
+
+
+ // Obscure Settings dialog
+ if(mUseRandomHostname)
+ cfg += "#my favorite options :)\nremote-random-hostname\n";
+
+ if(mUseFloat)
+ cfg+= "float\n";
+
+ if(mPersistTun) {
+ cfg+= "persist-tun\n";
+ cfg+= "# persist-tun also sets persist-remote-ip to avoid DNS resolve problem\n";
+ cfg+= "persist-remote-ip\n";
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean usesystemproxy = prefs.getBoolean("usesystemproxy", true);
+ if(usesystemproxy) {
+ cfg+= "# Use system proxy setting\n";
+ cfg+= "management-query-proxy\n";
+ }
+
+
+ if(mUseCustomConfig) {
+ cfg += "# Custom configuration options\n";
+ cfg += "# You are on your on own here :)\n";
+ cfg += mCustomConfigOptions;
+ cfg += "\n";
+
+ }
+
+
+
+ return cfg;
+ }
+
+ //! Put inline data inline and other data as normal escaped filename
+ private String insertFileData(String cfgentry, String filedata) {
+ if(filedata==null) {
+ return String.format("%s %s\n",cfgentry,"missing");
+ }else if(filedata.startsWith(VpnProfile.INLINE_TAG)){
+ String datawoheader = filedata.substring(VpnProfile.INLINE_TAG.length());
+ return String.format("<%s>\n%s\n</%s>\n",cfgentry,datawoheader,cfgentry);
+ } else {
+ return String.format("%s %s\n",cfgentry,openVpnEscape(filedata));
+ }
+ }
+
+ private boolean nonNull(String val) {
+ if(val == null || val.equals(""))
+ return false;
+ else
+ return true;
+ }
+
+ private Collection<String> getCustomRoutes() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutes==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutes.split("[\n \t]")) {
+ if(!route.equals("")) {
+ String cidrroute = cidrToIPAndNetmask(route);
+ if(cidrRoutes == null)
+ return null;
+
+ cidrRoutes.add(cidrroute);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+ private Collection<String> getCustomRoutesv6() {
+ Vector<String> cidrRoutes=new Vector<String>();
+ if(mCustomRoutesv6==null) {
+ // No routes set, return empty vector
+ return cidrRoutes;
+ }
+ for(String route:mCustomRoutesv6.split("[\n \t]")) {
+ if(!route.equals("")) {
+ cidrRoutes.add(route);
+ }
+ }
+
+ return cidrRoutes;
+ }
+
+
+
+ private String cidrToIPAndNetmask(String route) {
+ String[] parts = route.split("/");
+
+ // No /xx, assume /32 as netmask
+ if (parts.length ==1)
+ parts = (route + "/32").split("/");
+
+ if (parts.length!=2)
+ return null;
+ int len;
+ try {
+ len = Integer.parseInt(parts[1]);
+ } catch(NumberFormatException ne) {
+ return null;
+ }
+ if (len <0 || len >32)
+ return null;
+
+
+ long nm = 0xffffffffl;
+ nm = (nm << (32-len)) & 0xffffffffl;
+
+ String netmask =String.format("%d.%d.%d.%d", (nm & 0xff000000) >> 24,(nm & 0xff0000) >> 16, (nm & 0xff00) >> 8 ,nm & 0xff );
+ return parts[0] + " " + netmask;
+ }
+
+
+
+ private String[] buildOpenvpnArgv(File cacheDir)
+ {
+ Vector<String> args = new Vector<String>();
+
+ // Add fixed paramenters
+ //args.add("/data/data/se.leap.openvpn/lib/openvpn");
+ args.add(cacheDir.getAbsolutePath() +"/" + VpnProfile.MINIVPN);
+
+ args.add("--config");
+ args.add(cacheDir.getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ // Silences script security warning
+
+ args.add("script-security");
+ args.add("0");
+
+
+ return (String[]) args.toArray(new String[args.size()]);
+ }
+
+ public Intent prepareIntent(Context context) {
+ String prefix = context.getPackageName();
+
+ Intent intent = new Intent(context,OpenVpnService.class);
+
+ if(mAuthenticationType == VpnProfile.TYPE_KEYSTORE || mAuthenticationType == VpnProfile.TYPE_USERPASS_KEYSTORE) {
+ /*if(!saveCertificates(context))
+ return null;*/
+ }
+
+ intent.putExtra(prefix + ".ARGV" , buildOpenvpnArgv(context.getCacheDir()));
+ intent.putExtra(prefix + ".profileUUID", mUuid.toString());
+
+ ApplicationInfo info = context.getApplicationInfo();
+ intent.putExtra(prefix +".nativelib",info.nativeLibraryDir);
+
+ try {
+ FileWriter cfg = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + OVPNCONFIGFILE);
+ cfg.write(getConfigFile(context));
+ cfg.flush();
+ cfg.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return intent;
+ }
+
+ private boolean saveCertificates(Context context) {
+ PrivateKey privateKey = null;
+ X509Certificate[] cachain=null;
+ try {
+ privateKey = KeyChain.getPrivateKey(context,mAlias);
+ mPrivateKey = privateKey;
+
+ cachain = KeyChain.getCertificateChain(context, mAlias);
+ if(cachain.length <= 1 && !nonNull(mCaFilename))
+ OpenVPN.logMessage(0, "", context.getString(R.string.keychain_nocacert));
+
+ for(X509Certificate cert:cachain) {
+ OpenVPN.logInfo(R.string.cert_from_keystore,cert.getSubjectDN());
+ }
+
+
+
+
+ if(nonNull(mCaFilename)) {
+ try {
+ Certificate cacert = getCacertFromFile();
+ X509Certificate[] newcachain = new X509Certificate[cachain.length+1];
+ for(int i=0;i<cachain.length;i++)
+ newcachain[i]=cachain[i];
+
+ newcachain[cachain.length-1]=(X509Certificate) cacert;
+
+ } catch (Exception e) {
+ OpenVPN.logError("Could not read CA certificate" + e.getLocalizedMessage());
+ }
+ }
+
+
+ FileWriter fout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGCA);
+ PemWriter pw = new PemWriter(fout);
+ for(X509Certificate cert:cachain) {
+ pw.writeObject(new PemObject("CERTIFICATE", cert.getEncoded()));
+ }
+
+ pw.close();
+
+
+ if(cachain.length>= 1){
+ X509Certificate usercert = cachain[0];
+
+ FileWriter userout = new FileWriter(context.getCacheDir().getAbsolutePath() + "/" + VpnProfile.OVPNCONFIGUSERCERT);
+
+ PemWriter upw = new PemWriter(userout);
+ upw.writeObject(new PemObject("CERTIFICATE", usercert.getEncoded()));
+ upw.close();
+
+ }
+
+ return true;
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (KeyChainException e) {
+ OpenVPN.logMessage(0,"",context.getString(R.string.keychain_access));
+ }
+ return false;
+ }
+ private Certificate getCacertFromFile() throws FileNotFoundException, CertificateException {
+ CertificateFactory certFact = CertificateFactory.getInstance("X.509");
+
+ InputStream inStream;
+
+ if(mCaFilename.startsWith(INLINE_TAG))
+ inStream = new ByteArrayInputStream(mCaFilename.replace(INLINE_TAG,"").getBytes());
+ else
+ inStream = new FileInputStream(mCaFilename);
+
+ return certFact.generateCertificate(inStream);
+ }
+
+
+ //! Return an error if somethign is wrong
+ public int checkProfile(Context context) {
+/* if(mAuthenticationType==TYPE_KEYSTORE || mAuthenticationType==TYPE_USERPASS_KEYSTORE) {
+ if(mAlias==null)
+ return R.string.no_keystore_cert_selected;
+ }*/
+
+ if(!mUsePull) {
+ if(mIPv4Address == null || cidrToIPAndNetmask(mIPv4Address) == null)
+ return R.string.ipv4_format_error;
+ }
+ if(isUserPWAuth() && !nonNull(mUsername)) {
+ return R.string.error_empty_username;
+ }
+ if(!mUseDefaultRoute && getCustomRoutes()==null)
+ return R.string.custom_route_format_error;
+
+ // Everything okay
+ return R.string.no_error_found;
+
+ }
+
+ //! Openvpn asks for a "Private Key", this should be pkcs12 key
+ //
+ public String getPasswordPrivateKey() {
+ if(mTransientPCKS12PW!=null) {
+ String pwcopy = mTransientPCKS12PW;
+ mTransientPCKS12PW=null;
+ return pwcopy;
+ }
+ switch (mAuthenticationType) {
+ case TYPE_PKCS12:
+ case TYPE_USERPASS_PKCS12:
+ return mPKCS12Password;
+
+ case TYPE_CERTIFICATES:
+ case TYPE_USERPASS_CERTIFICATES:
+ return mKeyPassword;
+
+ case TYPE_USERPASS:
+ case TYPE_STATICKEYS:
+ default:
+ return null;
+ }
+ }
+ private boolean isUserPWAuth() {
+ switch(mAuthenticationType) {
+ case TYPE_USERPASS:
+ case TYPE_USERPASS_CERTIFICATES:
+ case TYPE_USERPASS_KEYSTORE:
+ case TYPE_USERPASS_PKCS12:
+ return true;
+ default:
+ return false;
+
+ }
+ }
+
+
+ public boolean requireTLSKeyPassword() {
+ if(!nonNull(mClientKeyFilename))
+ return false;
+
+ String data = "";
+ if(mClientKeyFilename.startsWith(INLINE_TAG))
+ data = mClientKeyFilename;
+ else {
+ char[] buf = new char[2048];
+ FileReader fr;
+ try {
+ fr = new FileReader(mClientKeyFilename);
+ int len = fr.read(buf);
+ while(len > 0 ) {
+ data += new String(buf,0,len);
+ len = fr.read(buf);
+ }
+ fr.close();
+ } catch (FileNotFoundException e) {
+ return false;
+ } catch (IOException e) {
+ return false;
+ }
+
+ }
+
+ if(data.contains("Proc-Type: 4,ENCRYPTED"))
+ return true;
+ else if(data.contains("-----BEGIN ENCRYPTED PRIVATE KEY-----"))
+ return true;
+ else
+ return false;
+ }
+
+ public int needUserPWInput() {
+ if((mAuthenticationType == TYPE_PKCS12 || mAuthenticationType == TYPE_USERPASS_PKCS12)&&
+ (mPKCS12Password == null || mPKCS12Password.equals(""))) {
+ if(mTransientPCKS12PW==null)
+ return R.string.pkcs12_file_encryption_key;
+ }
+
+ if(mAuthenticationType == TYPE_CERTIFICATES || mAuthenticationType == TYPE_USERPASS_CERTIFICATES) {
+ if(requireTLSKeyPassword() && !nonNull(mKeyPassword))
+ if(mTransientPCKS12PW==null) {
+ return R.string.private_key_password;
+ }
+ }
+
+ if(isUserPWAuth() && (mPassword.equals("") || mPassword == null)) {
+ if(mTransientPW==null)
+ return R.string.password;
+
+ }
+ return 0;
+ }
+
+ public String getPasswordAuth() {
+ if(mTransientPW!=null) {
+ String pwcopy = mTransientPW;
+ mTransientPW=null;
+ return pwcopy;
+ } else {
+ return mPassword;
+ }
+ }
+
+
+ // Used by the Array Adapter
+ @Override
+ public String toString() {
+ return mName;
+ }
+
+
+ public String getUUIDString() {
+ return mUuid.toString();
+ }
+
+
+ public PrivateKey getKeystoreKey() {
+ return mPrivateKey;
+ }
+
+
+
+}
+
+
+
+
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_add.png b/app/src/main/res/drawable-hdpi/ic_menu_add.png
new file mode 100644
index 00000000..444e8a5e
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_add.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_login.png b/app/src/main/res/drawable-hdpi/ic_menu_login.png
new file mode 100644
index 00000000..afa152b2
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_login.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.png b/app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..577e0558
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.png b/app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.png
new file mode 100644
index 00000000..c62295aa
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.png b/app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.png
new file mode 100644
index 00000000..47b4ba23
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/ic_vpn_disconnected.png b/app/src/main/res/drawable-hdpi/ic_vpn_disconnected.png
new file mode 100644
index 00000000..dfb962b9
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/ic_vpn_disconnected.png
Binary files differ
diff --git a/app/src/main/res/drawable-hdpi/icon.png b/app/src/main/res/drawable-hdpi/icon.png
new file mode 100644
index 00000000..02ede650
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/icon.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_menu_add.png b/app/src/main/res/drawable-ldpi/ic_menu_add.png
new file mode 100644
index 00000000..89620af8
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_menu_add.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_menu_login.png b/app/src/main/res/drawable-ldpi/ic_menu_login.png
new file mode 100644
index 00000000..d4181de5
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_menu_login.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_stat_vpn.png b/app/src/main/res/drawable-ldpi/ic_stat_vpn.png
new file mode 100644
index 00000000..f973015c
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_stat_vpn.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/ic_vpn_disconnected.png b/app/src/main/res/drawable-ldpi/ic_vpn_disconnected.png
new file mode 100644
index 00000000..22f3497e
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/ic_vpn_disconnected.png
Binary files differ
diff --git a/app/src/main/res/drawable-ldpi/icon.png b/app/src/main/res/drawable-ldpi/icon.png
new file mode 100644
index 00000000..e312075d
--- /dev/null
+++ b/app/src/main/res/drawable-ldpi/icon.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_add.png b/app/src/main/res/drawable-mdpi/ic_menu_add.png
new file mode 100644
index 00000000..361c7c46
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_add.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.png b/app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..f32a37e4
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.png b/app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.png
new file mode 100644
index 00000000..08291855
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.png b/app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.png
new file mode 100644
index 00000000..79281042
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/ic_vpn_disconnected.png b/app/src/main/res/drawable-mdpi/ic_vpn_disconnected.png
new file mode 100644
index 00000000..f8b02bfb
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/ic_vpn_disconnected.png
Binary files differ
diff --git a/app/src/main/res/drawable-mdpi/icon.png b/app/src/main/res/drawable-mdpi/icon.png
new file mode 100644
index 00000000..468314c6
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_login.png b/app/src/main/res/drawable-xhdpi/ic_menu_login.png
new file mode 100644
index 00000000..5095ed97
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_login.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.png
new file mode 100644
index 00000000..aa33c388
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.png b/app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.png
new file mode 100644
index 00000000..bd3fd784
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.png b/app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.png
new file mode 100644
index 00000000..a057db8b
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.png b/app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.png
new file mode 100644
index 00000000..7f44c46f
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.png
Binary files differ
diff --git a/app/src/main/res/drawable-xhdpi/icon.png b/app/src/main/res/drawable-xhdpi/icon.png
new file mode 100644
index 00000000..ead03720
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/icon.png
Binary files differ
diff --git a/app/src/main/res/layout-xlarge/about.xml b/app/src/main/res/layout-xlarge/about.xml
new file mode 100644
index 00000000..6ab88737
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/about.xml
@@ -0,0 +1,134 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/dashboardLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_marginLeft="12sp"
+ tools:context=".Dashboard" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/version"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:autoLink="all"
+ android:text="@string/copyright_leapgui" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:autoLink="all"
+ android:text="@string/repository_url_text" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:autoLink="all"
+ android:text="@string/translation_project_text" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:autoLink="all"
+ android:text="@string/translationby" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="18sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/copyright_others" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="12sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/openvpn"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ <!--
+ ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/openvpn_logo_descr"
+ android:src="@drawable/openvpnLogo" />
+ -->
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/opevpn_copyright" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/lzo"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/lzo_copyright" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/openssl"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="18sp"
+ android:text="@string/copyright_openssl" />
+ </LinearLayout>
+
+</ScrollView>
diff --git a/app/src/main/res/layout-xlarge/client_dashboard.xml b/app/src/main/res/layout-xlarge/client_dashboard.xml
new file mode 100644
index 00000000..bd644e1e
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/client_dashboard.xml
@@ -0,0 +1,69 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/dashboardLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_marginLeft="10sp"
+ android:layout_marginTop="10sp"
+ tools:context=".Dashboard" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="42dp"
+ android:background="?android:attr/selectableItemBackground" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="10dp" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/provider_label"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textSize="32sp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="32dp" >
+
+ <TextView
+ android:id="@+id/providerName"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/provider_label_none"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="1dp"
+ android:layout_marginBottom="7dp"
+ android:background="@android:drawable/divider_horizontal_bright" />
+
+ <LinearLayout
+ android:id="@+id/servicesCollection"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="0.11"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout-xlarge/configuration_wizard_activity.xml b/app/src/main/res/layout-xlarge/configuration_wizard_activity.xml
new file mode 100644
index 00000000..bb169e00
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/configuration_wizard_activity.xml
@@ -0,0 +1,27 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/configuration_wizard_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ConfigurationWizard" >
+
+ <ProgressBar
+ android:id="@+id/progressbar_configuration_wizard"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:max="3" />
+
+ <TextView
+ android:id="@+id/progressbar_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/configuring_provider"
+ android:textSize="24sp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_centerHorizontal="true"
+ android:textColor="@android:color/holo_blue_bright" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-xlarge/eip_service_fragment.xml b/app/src/main/res/layout-xlarge/eip_service_fragment.xml
new file mode 100644
index 00000000..e5c7f23d
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/eip_service_fragment.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp"
+ android:layout_marginLeft="20dp" >
+
+ <TextView
+ android:id="@+id/eipLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="10dp"
+ android:clickable="true"
+ android:text="@string/eip_service_label"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textSize="26sp" />
+
+ <Switch
+ android:id="@+id/eipSwitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="10dp"
+ android:height="26dp"/>
+
+ <ProgressBar
+ android:id="@+id/eipProgress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/Widget.Holo.ProgressBar.Horizontal"
+ android:indeterminate="true"
+ android:visibility="gone"
+ android:layout_below="@id/eipLabel"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp" />
+
+ <RelativeLayout
+ android:id="@+id/eipDetail"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/eipLabel"
+ android:paddingBottom="10dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ android:paddingTop="10dp"
+ android:visibility="gone" >
+
+ <ImageView
+ android:id="@+id/eipSettings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_margin="10dp"
+ android:contentDescription="@string/eip_settings_button_description"
+ android:src="@drawable/ic_sysbar_quicksettings" />
+
+ <TextView
+ android:id="@+id/eipStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:clickable="true"
+ android:text="@string/status_unknown"
+ android:textSize="16sp" />
+
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout-xlarge/log_in_dialog.xml b/app/src/main/res/layout-xlarge/log_in_dialog.xml
new file mode 100644
index 00000000..3a9eebb8
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/log_in_dialog.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:context=".LogInDialog" >
+
+ <TextView
+ android:id="@+id/user_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <EditText
+ android:id="@+id/username_entered"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="16dp"
+ android:ems="10"
+ android:hint="@string/username_hint"
+ android:inputType="textUri" >
+
+ <requestFocus />
+ </EditText>
+
+ <EditText
+ android:id="@+id/password_entered"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:ems="10"
+ android:hint="@string/password_hint"
+ android:inputType="textPassword" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-xlarge/logwindow.xml b/app/src/main/res/layout-xlarge/logwindow.xml
new file mode 100644
index 00000000..4051c92c
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/logwindow.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView android:text="@string/speed_waiting"
+ android:singleLine="true"
+ android:id="@+id/speed"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-xlarge/new_provider_dialog.xml b/app/src/main/res/layout-xlarge/new_provider_dialog.xml
new file mode 100644
index 00000000..fc7d84ab
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/new_provider_dialog.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:textSize="24sp" >
+
+ <EditText
+ android:id="@+id/new_provider_url"
+ android:inputType="textUri"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:textSize="24sp"
+ android:hint="@string/new_provider_uri" />
+
+ <CheckBox
+ android:id="@+id/danger_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:text="@string/danger_checkbox" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-xlarge/provider_detail_fragment.xml b/app/src/main/res/layout-xlarge/provider_detail_fragment.xml
new file mode 100644
index 00000000..4abbaa17
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/provider_detail_fragment.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/provider_detail_domain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="16dp"
+ android:textSize="32sp"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/provider_detail_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginBottom="4dp"
+ android:textSize="24sp"
+ android:textStyle="italic"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/provider_detail_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginBottom="4dp"
+ android:textSize="18sp"
+ android:textStyle="normal"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout-xlarge/provider_list_fragment.xml b/app/src/main/res/layout-xlarge/provider_list_fragment.xml
new file mode 100644
index 00000000..59dd37d1
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/provider_list_fragment.xml
@@ -0,0 +1,16 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:paddingTop="12dp" >
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:drawSelectorOnTop="false" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout-xlarge/provider_list_item.xml b/app/src/main/res/layout-xlarge/provider_list_item.xml
new file mode 100644
index 00000000..ec5db117
--- /dev/null
+++ b/app/src/main/res/layout-xlarge/provider_list_item.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:mode="twoLine"
+>
+
+ <TextView android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:layout_marginTop="6dip"
+ android:textSize = "32sp"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ />
+
+ <TextView android:id="@android:id/text2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/text1"
+ android:layout_alignLeft="@android:id/text1"
+ android:textSize = "24sp"
+ />
+
+</TwoLineListItem>
diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml
new file mode 100644
index 00000000..4b3f16e0
--- /dev/null
+++ b/app/src/main/res/layout/about.xml
@@ -0,0 +1,122 @@
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/dashboardLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_marginLeft="8sp"
+ tools:context=".Dashboard" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/version"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="all"
+ android:text="@string/copyright_leapgui" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="all"
+ android:text="@string/repository_url_text" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="all"
+ android:text="@string/translation_project_text" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:autoLink="all"
+ android:text="@string/translationby" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/copyright_others" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="10sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/openvpn"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+ <!--
+ ImageView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:contentDescription="@string/openvpn_logo_descr"
+ android:src="@drawable/openvpnLogo" />
+ -->
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/opevpn_copyright" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/lzo"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/lzo_copyright" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="20sp" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/openssl"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/copyright_openssl" />
+ </LinearLayout>
+
+</ScrollView>
diff --git a/app/src/main/res/layout/client_dashboard.xml b/app/src/main/res/layout/client_dashboard.xml
new file mode 100644
index 00000000..a5387efd
--- /dev/null
+++ b/app/src/main/res/layout/client_dashboard.xml
@@ -0,0 +1,67 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/dashboardLayout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:context=".Dashboard" >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="40dp"
+ android:background="?android:attr/selectableItemBackground" >
+
+ <LinearLayout
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="10dp" >
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/provider_label"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:textSize="24sp" />
+
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:orientation="vertical"
+ android:paddingLeft="15dp" >
+
+ <TextView
+ android:id="@+id/providerName"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:ellipsize="marquee"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/provider_label_none"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ </LinearLayout>
+ </LinearLayout>
+
+ <View
+ android:layout_width="wrap_content"
+ android:layout_height="2dp"
+ android:layout_marginBottom="15dp"
+ android:background="@android:drawable/divider_horizontal_bright" />
+
+ <LinearLayout
+ android:id="@+id/servicesCollection"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_weight="0.11"
+ android:orientation="vertical" >
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/configuration_wizard_activity.xml b/app/src/main/res/layout/configuration_wizard_activity.xml
new file mode 100644
index 00000000..f3d0e48b
--- /dev/null
+++ b/app/src/main/res/layout/configuration_wizard_activity.xml
@@ -0,0 +1,26 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/configuration_wizard_layout"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".ConfigurationWizard" >
+
+ <ProgressBar
+ android:id="@+id/progressbar_configuration_wizard"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:max="3" />
+
+ <TextView
+ android:id="@+id/progressbar_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fadingEdge="horizontal"
+ android:singleLine="true"
+ android:text="@string/configuring_provider"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:layout_centerHorizontal="true"
+ android:textColor="@android:color/holo_blue_bright" />
+
+</RelativeLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/eip_service_fragment.xml b/app/src/main/res/layout/eip_service_fragment.xml
new file mode 100644
index 00000000..5992a873
--- /dev/null
+++ b/app/src/main/res/layout/eip_service_fragment.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="10dp" >
+
+ <TextView
+ android:id="@+id/eipLabel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginLeft="10dp"
+ android:clickable="true"
+ android:text="@string/eip_service_label"
+ android:textAppearance="?android:attr/textAppearanceLarge" />
+
+ <Switch
+ android:id="@+id/eipSwitch"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_marginRight="10dp" />
+
+ <ProgressBar
+ android:id="@+id/eipProgress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@android:style/Widget.Holo.ProgressBar.Horizontal"
+ android:indeterminate="true"
+ android:visibility="gone"
+ android:layout_below="@id/eipLabel"
+ android:layout_marginLeft="15dp"
+ android:layout_marginRight="15dp" />
+
+ <RelativeLayout
+ android:id="@+id/eipDetail"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentRight="true"
+ android:layout_below="@+id/eipLabel"
+ android:paddingBottom="10dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ android:paddingTop="10dp"
+ android:visibility="gone" >
+
+ <ImageView
+ android:id="@+id/eipSettings"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentTop="true"
+ android:layout_margin="10dp"
+ android:contentDescription="@string/eip_settings_button_description"
+ android:src="@drawable/ic_sysbar_quicksettings" />
+
+ <TextView
+ android:id="@+id/eipStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_centerVertical="true"
+ android:clickable="true"
+ android:text="@string/status_unknown" />
+
+ </RelativeLayout>
+
+</RelativeLayout>
diff --git a/app/src/main/res/layout/log_in_dialog.xml b/app/src/main/res/layout/log_in_dialog.xml
new file mode 100644
index 00000000..c8a2f0a8
--- /dev/null
+++ b/app/src/main/res/layout/log_in_dialog.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ tools:context=".LogInDialog" >
+
+ <TextView
+ android:id="@+id/user_message"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <EditText
+ android:id="@+id/username_entered"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="16dp"
+ android:textSize="24sp"
+ android:ems="10"
+ android:hint="@string/username_hint"
+ android:inputType="textUri" >
+
+ <requestFocus />
+ </EditText>
+
+ <EditText
+ android:id="@+id/password_entered"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textSize="24sp"
+ android:ems="10"
+ android:hint="@string/password_hint"
+ android:inputType="textPassword" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/logwindow.xml b/app/src/main/res/layout/logwindow.xml
new file mode 100644
index 00000000..4051c92c
--- /dev/null
+++ b/app/src/main/res/layout/logwindow.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView android:text="@string/speed_waiting"
+ android:singleLine="true"
+ android:id="@+id/speed"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/new_provider_dialog.xml b/app/src/main/res/layout/new_provider_dialog.xml
new file mode 100644
index 00000000..19b8f442
--- /dev/null
+++ b/app/src/main/res/layout/new_provider_dialog.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <EditText
+ android:id="@+id/new_provider_url"
+ android:inputType="textUri"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginBottom="4dp"
+ android:hint="@string/new_provider_uri" />
+
+ <CheckBox
+ android:id="@+id/danger_checkbox"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/danger_checkbox" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/provider_detail_fragment.xml b/app/src/main/res/layout/provider_detail_fragment.xml
new file mode 100644
index 00000000..eb90fad9
--- /dev/null
+++ b/app/src/main/res/layout/provider_detail_fragment.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/provider_detail_domain"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="4dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginTop="16dp"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/provider_detail_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginBottom="4dp"
+ android:textStyle="italic"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <TextView
+ android:id="@+id/provider_detail_description"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:layout_marginLeft="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginBottom="4dp"
+ android:textStyle="normal"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/app/src/main/res/layout/provider_list_fragment.xml b/app/src/main/res/layout/provider_list_fragment.xml
new file mode 100644
index 00000000..70dbae0d
--- /dev/null
+++ b/app/src/main/res/layout/provider_list_fragment.xml
@@ -0,0 +1,15 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:paddingLeft="8dp"
+ android:paddingRight="8dp" >
+
+ <ListView
+ android:id="@id/android:list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1"
+ android:drawSelectorOnTop="false" />
+
+</LinearLayout>
diff --git a/app/src/main/res/layout/provider_list_item.xml b/app/src/main/res/layout/provider_list_item.xml
new file mode 100644
index 00000000..8746f6f8
--- /dev/null
+++ b/app/src/main/res/layout/provider_list_item.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<TwoLineListItem xmlns:android="http://schemas.android.com/apk/res/android"
+ android:paddingTop="2dip"
+ android:paddingBottom="2dip"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/activatedBackgroundIndicator"
+ android:minHeight="?android:attr/listPreferredItemHeight"
+ android:mode="twoLine"
+>
+
+ <TextView android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:layout_marginTop="6dip"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ />
+
+ <TextView android:id="@android:id/text2"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@android:id/text1"
+ android:layout_alignLeft="@android:id/text1"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ />
+
+</TwoLineListItem>
diff --git a/app/src/main/res/menu/client_dashboard.xml b/app/src/main/res/menu/client_dashboard.xml
new file mode 100644
index 00000000..676c07c7
--- /dev/null
+++ b/app/src/main/res/menu/client_dashboard.xml
@@ -0,0 +1,23 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/about_leap"
+ android:orderInCategory="110"
+ android:title="@string/about"/>
+ <item
+ android:id="@+id/switch_provider"
+ android:orderInCategory="501"
+ android:title="@string/switch_provider_menu_option"/>
+ <item
+ android:id="@+id/login_button"
+ android:showAsAction="ifRoom"
+ android:title="@string/login_button"
+ android:visible="false">
+ </item>
+ <item
+ android:id="@+id/logout_button"
+ android:showAsAction="ifRoom"
+ android:title="@string/logout_button"
+ android:visible="false">
+ </item>
+
+</menu>
diff --git a/app/src/main/res/menu/configuration_wizard_activity.xml b/app/src/main/res/menu/configuration_wizard_activity.xml
new file mode 100644
index 00000000..9936b6dc
--- /dev/null
+++ b/app/src/main/res/menu/configuration_wizard_activity.xml
@@ -0,0 +1,15 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <item
+ android:id="@+id/about_leap"
+ android:orderInCategory="110"
+ android:title="@string/about"/>
+
+ <item
+ android:id="@+id/new_provider"
+ android:orderInCategory="210"
+ android:title="@string/new_provider_button"
+ android:showAsAction="ifRoom|withText"
+ android:icon="@drawable/ic_menu_add"
+ />
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/logmenu.xml b/app/src/main/res/menu/logmenu.xml
new file mode 100644
index 00000000..c4087585
--- /dev/null
+++ b/app/src/main/res/menu/logmenu.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/clearlog"
+ android:icon="@drawable/ic_menu_trash_holo_light"
+ android:showAsAction="ifRoom|withText"
+ android:title="@string/clear_log"
+ android:titleCondensed="@string/clear"/>
+ <item
+ android:id="@+id/cancel"
+ android:icon="@android:drawable/ic_menu_close_clear_cancel"
+ android:showAsAction="ifRoom|withText"
+ android:title="@string/cancel_connection"
+ android:titleCondensed="@string/cancel"/>
+ <item
+ android:id="@+id/info"
+ android:icon="@android:drawable/ic_menu_info_details"
+ android:showAsAction="ifRoom|withText"
+ android:title="@string/show_connection_details"
+ android:titleCondensed="@string/info"/>
+
+ <item
+ android:id="@+id/send"
+ android:icon="@android:drawable/ic_menu_share"
+ android:showAsAction="ifRoom|withText"
+ android:title="@string/send_logfile"
+ android:titleCondensed="@string/send"/>
+ <item
+ android:alphabeticShortcut="e"
+ android:icon="@android:drawable/ic_menu_edit"
+ android:showAsAction="withText|ifRoom"
+ android:title="@string/edit_vpn"/>
+
+</menu>
diff --git a/app/src/main/res/values-ca/arrays.xml b/app/src/main/res/values-ca/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-ca/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
new file mode 100644
index 00000000..2b250da1
--- /dev/null
+++ b/app/src/main/res/values-ca/strings.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Cancel·la</string>
+ <string name="about">Quan a</string>
+ <string name="no_error_found">No s\'ha trobat cap error</string>
+ <string name="config_error_found">S\'ha trobat un error a la configuració</string>
+ <string name="vpn_launch_title">Conecta a la VPN</string>
+ <string name="shortcut_profile_notfound">No s\'ha trobat el perfil especificat a l\'accès directe</string>
+ <string name="route_rejected">La ruta ha estat refusas per Android</string>
+ <string name="cancel_connection">Desconecta</string>
+ <string name="clear_log">Neteja el registre</string>
+ <string name="title_cancel">Cancela la confirmació</string>
+ <string name="cancel_connection_query">Desconcta la conexió VPN/cancela l\'intent de conexió?</string>
+ <string name="edit_vpn">Edita la configuració VPN</string>
+ <string name="error">"Error: "</string>
+ <string name="clear">Neteja</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Mostra els detalls de conexió</string>
+ <string name="dns_server_info">Servidor DNS: %s</string>
+ <string name="dns_domain_info">Domini DNS: %s</string>
+ <string name="routes_info">Rutes: %s</string>
+ <string name="routes_info6">Rutes IPv6: %s</string>
+ <string name="send_logfile">Envia el fitxer de registre</string>
+ <string name="send">Envia</string>
+ <string name="bitmask_openvpn_log_file">Fitxer de registre de LEAP Android</string>
+ <string name="copied_entry">S\'ha copiat l\'entrada al porta-retalls</string>
+ <string name="speed_waiting">Esperant el missatge d\'estat...</string>
+ <string name="converted_profile">Perfil importat</string>
+ <string name="converted_profile_i">Perfil importat %d</string>
+ <string name="private_key_password">Contrasenya de la clau privada</string>
+ <string name="password">Contrasenya</string>
+ <string name="building_configration">Construint la configuració...</string>
+ <string name="netstatus">Estat de la xarxa %s</string>
+ <string name="translationby">Traducció al catala per Sergi Almacellas
+&lt;sergi@koolpi.com&gt;</string>
+ <string name="using_proxy">Utilitzant el proxy %1$s %2$d</string>
+ <string name="ignore">Ignorar</string>
+ <string name="restart">Reinicia</string>
+ <string name="restart_vpn_after_change">Els canvis de configuració s\'apliquen desprès de reinicar la VPN. (Re)inicar la VPN ara?</string>
+ <string name="configuration_changed">S\'ha canviat la configuració</string>
+</resources>
diff --git a/app/src/main/res/values-cs/arrays.xml b/app/src/main/res/values-cs/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-cs/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
new file mode 100644
index 00000000..68c5aeaf
--- /dev/null
+++ b/app/src/main/res/values-cs/strings.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Storno</string>
+ <string name="repository_url_text">Zdrojové kódy a seznam problémů je na https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Tento program používá následující komponenty; viz zdrojový kód pro detaily o licenci</string>
+ <string name="about">O programu</string>
+ <string name="no_error_found">Bez chyb</string>
+ <string name="config_error_found">Chyba v konfiguraci</string>
+ <string name="ipv4_format_error">Chyba při zpracování IPv4 adresy</string>
+ <string name="custom_route_format_error">Chyba při zpracování vlastního směrování</string>
+ <string name="shortcut_profile_notfound">Profil zvolený ve zkratce nenalezen</string>
+ <string name="route_rejected">Směrování odmítnuto Androidem</string>
+ <string name="cancel_connection">Odpojit</string>
+ <string name="clear_log">vymazat log</string>
+ <string name="title_cancel">Zrušit potvrzení</string>
+ <string name="cancel_connection_query">Odpojit/Zrušit připojování?</string>
+ <string name="edit_vpn">Změnit nastavení VPN</string>
+ <string name="tun_error_helpful">Na některých ICS systémech může být oprávnění pro /dev/tun špatně nastavené, nebo tun modul může zcela chybět.</string>
+ <string name="tun_open_error">Chyba při otvírání tun zařízení</string>
+ <string name="error">"Chyba: "</string>
+ <string name="clear">Vymazat</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Zobrazit detaily spojení</string>
+ <string name="last_openvpn_tun_config">Poslední nastavení rozhraní pro OpenVPN:</string>
+ <string name="local_ip_info">Místní IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS server: %s</string>
+ <string name="dns_domain_info">DNS doména: %s</string>
+ <string name="routes_info">Směrování: %s</string>
+ <string name="routes_info6">Směrování IPv6: %s</string>
+ <string name="ip_not_cidr">Získány informace o rozhraní %1$s a %2$s, předpokládám, že druhá adresa je adresa vzdáleného kolegy. Používám /32 masku pro místní IP adresu. Mód OpenVPN je \"%3$s\".</string>
+ <string name="route_not_cidr">%1$s a %2$s jako IP adresy s CIDR maskou nedávají smysl, používám /32 jako masku.</string>
+ <string name="route_not_netip">Směrování opraveno z %1$s/%2$s na %3$s/%2$s</string>
+ <string name="keychain_access">Nelze přistoupit k Androidímu úložišti certifikátů. To může být způsobeno aktualizací firmwaru nebo obnovováním aplikace a jejího nastavení ze zálohy. Uprav VPN profil a znovu vyber certifikát pro vytvoření patřičných povolení.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Odeslat soubor s logem</string>
+ <string name="send">Odeslat</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android logovací soubor</string>
+ <string name="copied_entry">Záznam z logu zkopírován do schránky</string>
+ <string name="no_vpn_support_image">Tvůj obrázek není podporovaný rozhraním VPNService, je mi líto :-(</string>
+ <string name="opentun_no_ipaddr">Odmítám otevřít tun zařízení bez informace o IP</string>
+ <string name="speed_waiting">Čekám na zprávu o stavu…</string>
+ <string name="converted_profile">importovaný profil</string>
+ <string name="converted_profile_i">importovaný profil %d</string>
+ <string name="error_empty_username">Jméno nesmí být prázdné.</string>
+ <string name="pkcs12_file_encryption_key">Šifrovací klíč PKCS12</string>
+ <string name="private_key_password">Heslo k soukromému klíči</string>
+ <string name="password">Heslo</string>
+ <string name="building_configration">Vytvářím konfiguraci…</string>
+ <string name="cert_from_keystore">Získán certifikát \'%s\' z úložiště</string>
+ <string name="netstatus">Stav sítě: %s</string>
+ <string name="keychain_nocacert">Žádný CA certifikát nebyl získán z úložiště, autentikace pravděpodobně selže.</string>
+ <string name="mobile_info">Spuštěno na %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Chyba při podepisování klíčem %1$s: %2$s</string>
+ <string name="translationby">Českou lokalizaci zpracoval Jan Baier &lt;baier.jan@gmail.com&gt;</string>
+ <string name="warn_no_dns">Není použit DNS server. Překlad jmen nemusí fungovat. Zvaž nastavení vlastního DNS serveru.</string>
+ <string name="dns_add_error">Nemohu přidat DNS server \"%1$s\", odmítnuto systémem: %2$s</string>
+ <string name="getproxy_error">Chyba při zjišťování nastavení proxy: %s</string>
+ <string name="using_proxy">Používám proxy %1$s %2$d</string>
+ <string name="ignore">Ignorovat</string>
+ <string name="restart">Restartovat</string>
+ <string name="restart_vpn_after_change">Změna nastavení začne platit až po restartu VPN. Restartovat teď?</string>
+ <string name="configuration_changed">Nastavení změněno</string>
+ <string name="minidump_generated">OpenVPN neočekávaně havarovalo. Zvaž možnost použití volby poslat Minidump z hlavního menu</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+ <string name="state_connecting">Připojuji se</string>
+ <string name="state_wait">Čekání na odpověď serveru</string>
+ <string name="state_auth">Ověřuji autorizaci</string>
+ <string name="state_get_config">Stahuji konfiguraci klienta</string>
+ <string name="state_assign_ip">Nastavuji IP adresu</string>
+ <string name="state_add_routes">Přidávám trasy</string>
+ <string name="state_connected">Připojeno</string>
+ <string name="state_reconnecting">Obnovuji připojení</string>
+ <string name="state_exiting">Ukončuji</string>
+ <string name="state_noprocess">Neběží</string>
+ <string name="state_resolve">Překlad názvů</string>
+ <string name="state_tcp_connect">Připojuji (TCP)</string>
+ <string name="state_auth_failed">Přihlášení nebylo úspěšné</string>
+ <string name="state_nonetwork">Čekání na použitelnou síť</string>
+</resources>
diff --git a/app/src/main/res/values-de/arrays.xml b/app/src/main/res/values-de/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-de/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
new file mode 100644
index 00000000..84d93cfe
--- /dev/null
+++ b/app/src/main/res/values-de/strings.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Abbrechen</string>
+ <string name="repository_url_text">Quellcode und Issue Tracker sind verfügbar unter https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Dieses Programm nutzt die folgenden Komponenten. Die kompletten Lizenzdetails sind im Quelltext verfügbar.</string>
+ <string name="about">Über</string>
+ <string name="no_error_found">Kein Fehler.</string>
+ <string name="config_error_found">Fehler in der Konfiguration</string>
+ <string name="ipv4_format_error">Kann die die IPv4 Adresse nicht parsen</string>
+ <string name="custom_route_format_error">Kann die manuell angegeben Routen nicht parsen</string>
+ <string name="shortcut_profile_notfound">Von der Verknüpfung referenziertes Profil konnte nicht gefunden werden</string>
+ <string name="route_rejected">Route von Android zurückgewiesen.</string>
+ <string name="cancel_connection">Trennen</string>
+ <string name="clear_log">Log löschen.</string>
+ <string name="title_cancel">Trennungsbestätigung</string>
+ <string name="cancel_connection_query">Möchten Sie das VPN trennen bzw. den Verbindungsversuch abbrechen?</string>
+ <string name="edit_vpn">Ändere VPN Einstellungen</string>
+ <string name="tun_error_helpful">Auf manchen ROM Version sind eventuell die Zugriffsrechte von /dev/tun falsch oder das tun Kernel Modul fehlt. Für Cyanogenmod 9 ROMs mit root gibt einen provisorischen Fix in den generellen Einstellungen.</string>
+ <string name="tun_open_error">Das Öffnen des tun Interfaces ist katastrophal gescheitert</string>
+ <string name="error">"Fehler: "</string>
+ <string name="clear">Clear</string>
+ <string name="info">Info</string>
+ <string name="show_connection_details">Zeige Verbindungsdetails</string>
+ <string name="last_openvpn_tun_config">Letzte Interface Konfiguration von OpenVPN:</string>
+ <string name="local_ip_info">Lokale IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS Server: %s</string>
+ <string name="dns_domain_info">DNS Domäne: %s</string>
+ <string name="routes_info">Routen IPv4: %s</string>
+ <string name="routes_info6">Routen IPv6: %s</string>
+ <string name="ip_not_cidr">Interface Information %1$s und %2$s, nehme an, die zweite Adresse ist die Peer Adresse. Benutze /32 Netzmaske für die lokale IP Adresse. Interface Modus spezifiziert von OpenVPN ist \"%3$s\".</string>
+ <string name="route_not_cidr">Die Route %1$s mit der Netzmaske %2$s ist keine Route mit einer CIDR Netzmaske, benutze /32 als Netzmaske.</string>
+ <string name="route_not_netip">Route %1$s/%2$s korrigiert zu %3$s/%2$s</string>
+ <string name="keychain_access">Kann nicht auf die Android Keychain Zertifikate zugreifen (dies kann durch ein System Update oder durch Zurücksichern der Anwendung aus einem Backup hervorgerufen werden). Bitte editieren Sie das VPN und wählen Sie erneut das Zertifikat in dem Grundeinstellungen aus, um die Zugriffsberechtigung für das Zertifikat wieder herzustellen.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Sende Logdatei</string>
+ <string name="send">Sende</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android log Datei</string>
+ <string name="copied_entry">Log Eintrag in die Zwischenablage kopiert</string>
+ <string name="no_vpn_support_image">Dieses Android ROM enthält keine VPNService API. Sorry :(</string>
+ <string name="opentun_no_ipaddr">Verweigere tun Gerät zu öffnen ohne IP Information</string>
+ <string name="speed_waiting">Warte auf OpenVPN Status Nachricht…</string>
+ <string name="converted_profile">Importiertes Profil</string>
+ <string name="converted_profile_i">Importiertes Profil %d</string>
+ <string name="error_empty_username">Der Benutzername darf nicht leer sein</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 Veschlüsslungspassword</string>
+ <string name="private_key_password">Passphrase privater Schlüssel</string>
+ <string name="password">Passwort</string>
+ <string name="building_configration">Generiere OpenVPN Konfiguration…</string>
+ <string name="cert_from_keystore">Zertifikat (KeyStore): \'%s\' </string>
+ <string name="netstatus">Netzwerkstatus: %s</string>
+ <string name="keychain_nocacert">Beim Abfragen des Android KeyStore wurde kein CA Zertifikat zurückgegeben. Überprüfen des Serverzertifikat wird wahrscheinlich fehlschlagen. Geben Sie manuell ein CA Zertifikat an.</string>
+ <string name="mobile_info">Modell %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Fehler beim Zugriff auf den Android Keystore %1$s: %2$s</string>
+ <string name="translationby">Deutsche Übersetzung von Arne Schwabe &lt;arne@rfc2549.org&gt;</string>
+ <string name="warn_no_dns">Es werden keine DNS Server gesetzt. Möglicherweise wird die DNS Auflösung nicht funktionieren. Ziehen Sie in Betracht die Option \"eigene DNS Server\" zu benutzen.</string>
+ <string name="dns_add_error">Konnte den DNS Server \"%1$s\" nicht hinzufügen, da das System ihn zurückweist mit %2$s</string>
+ <string name="getproxy_error">Fehler beim Ermitteln der Proxy Einstellungen: %s</string>
+ <string name="using_proxy">Benutzt Proxy %1$s %2$d</string>
+ <string name="ignore">Ignorieren</string>
+ <string name="restart">Neu verbinden</string>
+ <string name="restart_vpn_after_change">Konfigurationsänderungen werden erst nach einem VPN Neustart aktiv. Jetzt neu verbinden?</string>
+ <string name="configuration_changed">Konfiguration geändert</string>
+ <string name="minidump_generated">Der OpenVPN Prozess ist unerwartet abgestürzt. Bitte erwägen Sie die Option \"Minidump senden\" im Hauptmenü</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+
+ <string name="state_connecting">Verbinde</string>
+ <string name="state_wait">Warte auf Serverantwort</string>
+ <string name="state_auth">Authentifiziere</string>
+ <string name="state_get_config">Warte auf Clientkonfiguration</string>
+ <string name="state_assign_ip">Weise IP Adressen zu</string>
+ <string name="state_add_routes">Hinzufügen von Routen</string>
+ <string name="state_connected">Verbunden</string>
+ <string name="state_reconnecting">Wiederverbinden</string>
+ <string name="state_exiting">Beende</string>
+ <string name="state_noprocess">Kein OpenVPN Prozess</string>
+ <string name="state_resolve">Löse Hostnamen auf</string>
+ <string name="state_tcp_connect">Verbinde (TCP)</string>
+ <string name="state_auth_failed">Authentifizierung fehlgeschlagen</string>
+ <string name="state_nonetwork">Warte auf Internetverbindung</string>
+</resources>
diff --git a/app/src/main/res/values-es/arrays.xml b/app/src/main/res/values-es/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-es/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
new file mode 100644
index 00000000..f8046a3f
--- /dev/null
+++ b/app/src/main/res/values-es/strings.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Cancelar</string>
+ <string name="repository_url_text">Codigo fuente y sistema de reporte de errores disponibles en https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">El programa utiliza los siguientes componentes. Vea los códigos fuentes para obtener más información sobre las licencias</string>
+ <string name="about">Acerca de</string>
+ <string name="route_rejected">Ruta rechazada por Android</string>
+ <string name="cancel_connection">Desconectar</string>
+ <string name="clear_log">Limpiar registro</string>
+ <string name="title_cancel">Cancelar confirmación</string>
+ <string name="cancel_connection_query">¿Desconectar la conexión VPN/cancelar el intento de conexión?</string>
+ <string name="edit_vpn">Modificar la configuración de VPN</string>
+ <string name="tun_error_helpful">En algunas imágenes personalizadas de ICS los permisos sobre /dev/tun podrían ser incorrectos, o el módulo tun podría faltar completamente.</string>
+ <string name="tun_open_error">La apertura de la interfaz tun falĺó</string>
+ <string name="error">"Error: "</string>
+ <string name="clear">Borrar</string>
+ <string name="info">información</string>
+ <string name="show_connection_details">Mostrar detalles de la conexión</string>
+ <string name="last_openvpn_tun_config">Última configuración de interfaz de OpenVPN:</string>
+ <string name="local_ip_info">Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">Servidor DNS: %s</string>
+ <string name="dns_domain_info">Dominio DNS: %s</string>
+ <string name="routes_info">Rutas: %s</string>
+ <string name="routes_info6">Rutas IPv6: %s</string>
+ <string name="ip_not_cidr">Información de la interfaz obtenida %1$s and %2$s, asumiendo que la segunda dirección es una dirección equivalente del remoto. Usando una máscara de red /32 para la IP local. El modo dado por OpenVPN es \"%3$s\".</string>
+ <string name="route_not_cidr">No tienen sentido %1$s y %2$s como ruta IP con máscara de red CIDR, usando /32 como máscara de red.</string>
+ <string name="route_not_netip">Ruta conectada de %1$s/%2$s a %3$s/%2$s</string>
+ <string name="keychain_access">No se puede acceder a los certificados de Android. Puede ser causado por una actualización de firmware o por restaurar una copia de seguridad de la aplicación/configuración de la aplicación. Por favor edite la VPN y vuelva a seleccionar el certificado bajo meltdownajustes básicos para recrear los permisos para acceder al certificado.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Enviar el archivo de registro</string>
+ <string name="send">Enviar</string>
+ <string name="bitmask_openvpn_log_file">Archivo de registro de LEAP de Android</string>
+ <string name="copied_entry">Entrada de registro copiada al Portapapeles</string>
+ <string name="no_vpn_support_image">Su imagen no es compatible con la API de VPNService, lo siento :(</string>
+ <string name="opentun_no_ipaddr">Negandose a abrir el dispositivo tun sin información de IP</string>
+ <string name="speed_waiting">Esperando el mensaje de estado...</string>
+ <string name="converted_profile">perfil importado</string>
+ <string name="converted_profile_i">perfil importado %d</string>
+ <string name="error_empty_username">El nombre de usuario no debe estar vacío.</string>
+ <string name="pkcs12_file_encryption_key">Clave PKCS12 de cifrado de archivos</string>
+ <string name="private_key_password">Contraseña de clave privada</string>
+ <string name="password">Contraseña</string>
+ <string name="building_configration">Construyendo configuracion...</string>
+ <string name="cert_from_keystore">Conseguido el certificado de \'%s\' de almacén de claves</string>
+ <string name="netstatus">Estado de la red: %s</string>
+ <string name="keychain_nocacert">No se obtuvo ningún certificado de CA al leer el almacén de claves de Android. La autenticación probablemente fallará.</string>
+ <string name="mobile_info">Ejecutándose en %1$s (%2$s) %3$s, API de Android %4$d</string>
+ <string name="error_rsa_sign">Error al firmar con la llave del almacén de llaves de Android %1$s: %2$s</string>
+ <string name="translationby">Traducción al español por José Luis Bandala Perez&lt;luis.449bp@gmail.com&gt;</string>
+ <string name="warn_no_dns">Sin servidores DNS utilizados. La resolución de nombres puede que no funcione. Considere configurar servidores DNS personalizados</string>
+ <string name="dns_add_error">No se puede agregar el servidor DNS \"%1$s\", rechazado por el sistema: %2$s</string>
+ <string name="getproxy_error">Error al obtener la configuración de proxy: %s</string>
+ <string name="using_proxy">Usando proxy %1$s %2$d</string>
+ <string name="ignore">Ignorar</string>
+ <string name="restart">Reiniciar</string>
+ <string name="restart_vpn_after_change">Los cambios de configuración se aplican después de reiniciar la VPN. ¿(Re)iniciar la VPN ahora?</string>
+ <string name="configuration_changed">Configuración cambiada</string>
+
+ <string name="minidump_generated">OpenVPN falló inesperadamente. Por favor considere usar la opción envío de minivolcado en el menú principal</string>
+</resources>
diff --git a/app/src/main/res/values-et/arrays.xml b/app/src/main/res/values-et/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-et/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml
new file mode 100644
index 00000000..04dc9572
--- /dev/null
+++ b/app/src/main/res/values-et/strings.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Tühista</string>
+ <string name="repository_url_text">Lähtetekst ja probleemihaldur asuvad veebilehel https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Programmis kasutatakse järgnevaid komponente. Detailse litsenseerimisinfo leiate lähtekoodist</string>
+ <string name="about">Lähemalt</string>
+ <string name="no_error_found">Vigu ei leitud</string>
+ <string name="config_error_found">Konfiguratsiooni viga</string>
+ <string name="ipv4_format_error">Sisestatud IPv4 aadress ei allu süntaksianalüüsile</string>
+ <string name="custom_route_format_error">Kohandatud marsruudid ei allu süntaksianalüüsile</string>
+ <string name="vpn_launch_title">Ühendu VPN\'iga</string>
+ <string name="shortcut_profile_notfound">Lühivalikus määratud profiil puudub</string>
+ <string name="route_rejected">Androidi poolt keelatud ruutingud</string>
+ <string name="cancel_connection">Katkesta ühendus</string>
+ <string name="clear_log">Tühjenda logi</string>
+ <string name="title_cancel">Loobu kinnitusest</string>
+ <string name="cancel_connection_query">Katkesta VPN ühendus/tühista ühendumise katse?</string>
+ <string name="edit_vpn">Muuda VPN seadistusi</string>
+ <string name="tun_error_helpful">Mõnel modifitseeritud ICS versioonil võivad /dev/tun õigused olla valed, või selle moodul sootuks puududa.</string>
+ <string name="tun_open_error">Tun liidese avamine ebaõnnestus</string>
+ <string name="error">"Viga:"</string>
+ <string name="clear">Tühjenda</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Näita ühenduse andmeid</string>
+ <string name="last_openvpn_tun_config">Viimane liidese konfigureerimine OpenVPN poolt:</string>
+ <string name="local_ip_info">Lokaalne IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS Server: %s</string>
+ <string name="dns_domain_info">DNS domeen: %s</string>
+ <string name="routes_info">Marsruudid: %s</string>
+ <string name="routes_info6">IPv6 marsruudid: %s</string>
+ <string name="ip_not_cidr">Liidese andmed on %1$s ja %2$s, eeldades et teine aadress on eemalasuva serveri aadress. Lokaalse IP jaoks kasutatakse /32 võrgumaski. OpenVPN teatab režiimiks %3$s\".</string>
+ <string name="route_not_cidr">%1$s ja %2$s on mõttetud CIDR võrgumaskiga IP marsruutidest, võrgumaskiks määratakse /32.</string>
+ <string name="route_not_netip">%1$s/%2$s marsruut parandatud: %3$s/%2$s</string>
+ <string name="keychain_access">Androidi Keychain sertifikaadid on kättesaamatud. See võib olla põhjustatud püsivara uuendamisest või appide/apiseadistuste taastamisest. Sertifikaatide pääsuõiguste taastamiseks redigeerige palun VPN seadistusi ja valige uuesti üldiste seadistuste alt sertifikaat.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Saada logifail</string>
+ <string name="send">Saada</string>
+ <string name="bitmask_openvpn_log_file">LEAP Androidile logifail</string>
+ <string name="copied_entry">Logikirje kopeeriti lõikepuhvrisse</string>
+ <string name="no_vpn_support_image">Teie süsteemitarkvara ei toeta VPNService API\'t, vabandame :(</string>
+ <string name="opentun_no_ipaddr">IP andmeteta keeldutakse tun liidese avamisest</string>
+ <string name="speed_waiting">Ootan olekuteadet…</string>
+ <string name="converted_profile">imporditud profiil</string>
+ <string name="converted_profile_i">imporditud profiil %d</string>
+ <string name="error_empty_username">Kasutajanimi peab olema määratud.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 faili krüpteerimisvõti</string>
+ <string name="private_key_password">Privaatse võtme salasõna</string>
+ <string name="password">Salasõna</string>
+ <string name="building_configration">Koostatakse konfiguratsiooni…</string>
+ <string name="cert_from_keystore">Saadud sertifikaat \'%s\' võtmehoidlast</string>
+ <string name="netstatus">Võrgu olek: %s</string>
+ <string name="keychain_nocacert">Androidi võtmehoidlast lugemine ei andnud ühtegi CA sertifikaati. Suure tõenäosusega autentimine ebaõnnestub.</string>
+ <string name="mobile_info">Töötamas %1$s (%2$s) %3$s peal, Android API %4$d</string>
+ <string name="error_rsa_sign">Viga allkirjastamisel Androidi võtmehoidla võtmega %1$s: %2$s</string>
+ <string name="translationby">Eesti keelde tõlkis Robert Tiismus</string>
+ <string name="warn_no_dns">Ühtegi DNS serverit ei kasutata. Nimelahendus ei pruugi töötada. Vae kohandatud DNS serveri kasutust</string>
+ <string name="dns_add_error">DNS serveri \"%1$s\" lisamine ebaõnnestus, süsteemi poolt keelduti: %2$s</string>
+ <string name="getproxy_error">Viga proxy seadistuste vastuvõtul: %s</string>
+ <string name="using_proxy">Kasutusel proxy %1$s %2$d</string>
+ <string name="ignore">Ignoreeri</string>
+ <string name="restart">Uuestilaadimine</string>
+ <string name="restart_vpn_after_change">Konfiguratsioonimuudatused rakendatakse peale VPN uuestilaadimist. Kas soovite VPN kohe (uuesti)laadida?</string>
+ <string name="configuration_changed">Konfiguratsiooni muudeti</string>
+
+ <string name="minidump_generated">OpenVPN jooksis ootamatult kokku. Palun kaaluge \"saada Minitõmmis\" valiku lubamist peamenüüs</string>
+</resources>
diff --git a/app/src/main/res/values-fr/arrays.xml b/app/src/main/res/values-fr/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-fr/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
new file mode 100644
index 00000000..79e4d70c
--- /dev/null
+++ b/app/src/main/res/values-fr/strings.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">"Annuler"</string>
+ <string name="repository_url_text">"Le code source et le tracker de bugs est disponible ici: https://github.com/leapcode/bitmask_android/ "</string>
+ <string name="copyright_others">"Le programme utilise les composants suivants. Voir le code source pour plus de détails sur les licences."</string>
+ <string name="about">"À propos"</string>
+ <string name="no_error_found">"Aucune erreur"</string>
+ <string name="config_error_found">"Erreur dans la configuration"</string>
+ <string name="ipv4_format_error">"Impossible d\'analyser l\'adresse IPv4"</string>
+ <string name="custom_route_format_error">"Impossible d\'analyser les règles de redirection personnalisés"</string>
+ <string name="vpn_launch_title">"Se connecter au VPN"</string>
+ <string name="shortcut_profile_notfound">"Profil spécifié dans raccourci introuvable"</string>
+ <string name="route_rejected">"Route rejetée par Android"</string>
+ <string name="cancel_connection">"Déconnecter"</string>
+ <string name="clear_log">"Effacer les logs"</string>
+ <string name="title_cancel">"Annuler la confirmation"</string>
+ <string name="cancel_connection_query">"Déconnecter le VPN connecté / annuler la tentative de connexion ?"</string>
+ <string name="edit_vpn">"Modifier les paramètres VPN"</string>
+ <string name="tun_error_helpful">"Sur certaines ROMs ICS les permissions de /dev/tun peuvent être incorrectes, ou le module Tun peut être manquant."</string>
+ <string name="tun_open_error">"L\'ouverture de l\'interface Tun a échoué."</string>
+ <string name="error">"Erreur: "</string>
+ <string name="clear">"Effacer"</string>
+ <string name="info">"infos"</string>
+ <string name="show_connection_details">"Afficher les détails de la connexion"</string>
+ <string name="last_openvpn_tun_config">"Dernière configuration connue de l\'interface d\'OpenVPN:"</string>
+ <string name="local_ip_info">"IPv4 locale: %1$s/%2$d IPv6: %3$s MTU: %4$d"</string>
+ <string name="dns_server_info">"Serveur DNS: %s"</string>
+ <string name="dns_domain_info">"Domaine DNS: %s"</string>
+ <string name="routes_info">"Routes: %s"</string>
+ <string name="routes_info6">"Routes IPv6: %s"</string>
+ <string name="ip_not_cidr">"Informations récupérées de l\'interface: %1$s et %2$s , en supposant que la seconde adresse est l\'adresse peer du réseau distant. Utilisation du masque de réseau /32 pour l\'IP locale. Mode donné par OpenVPN: \"%3$s\"."</string>
+ <string name="route_not_cidr">"Ne peut pas donner un sens à %1$s et %2$s comme routage IP avec masque réseau de type CIDR, en utilisant /32 comme masque de réseau."</string>
+ <string name="route_not_netip">"Règle de redirection corrigée: %1$s / %2$s en %3$s / %2$s"</string>
+ <string name="keychain_access">\"Impossible d\'accéder aux certificats \"Android Keychain\". (Peut être causé par une mise à jour du firmware ou par une restauration d\'une sauvegarde des paramètres de l\'application). Veuillez modifier le profil VPN et sélectionnez de nouveau le certificat dans les réglages de base pour recréer l\'autorisation d\'accéder au certificat.\".</string>
+ <string name="version_info">"%1$s %2$s"</string>
+ <string name="send_logfile">"Envoyer le fichier de log"</string>
+ <string name="send">"Envoyer"</string>
+ <string name="bitmask_openvpn_log_file">"Fichier de log LEAP Android"</string>
+ <string name="copied_entry">"Entrée du log copiée"</string>
+ <string name="no_vpn_support_image">"Votre ROM ne prend pas en charge l\'API VPNService, désolé :("</string>
+ <string name="opentun_no_ipaddr">"Impossible d\'ouvrir le périphérique TUN sans informations IP"</string>
+ <string name="speed_waiting">"Attente du status..."</string>
+ <string name="converted_profile">"profil importé"</string>
+ <string name="converted_profile_i">"profil importé: %d"</string>
+ <string name="error_empty_username">"Le nom d\'utilisateur ne doit pas être vide."</string>
+ <string name="pkcs12_file_encryption_key">"Fichier de clé de cryptage PKCS12"</string>
+ <string name="private_key_password">"Mot de passe de clé privée"</string>
+ <string name="password">"Mot de passe"</string>
+ <string name="building_configration">"Création de la configuration ..."</string>
+ <string name="cert_from_keystore">"Certificat \'%s\' obtenu"</string>
+ <string name="netstatus">"État du réseau: %s"</string>
+ <string name="keychain_nocacert">"Aucun certificat CA renvoyée lors de la lecture depuis le gestionnaire de clés. L\'authentification échouera probablement."</string>
+ <string name="mobile_info">"Fonctionnant sur %1$s (%2$s) %3$s , Android API %4$d"</string>
+ <string name="error_rsa_sign">"Erreur de signature de la clé %1$s : %2$s par le gestionnaire d\'Android"</string>
+ <string name="translationby">French translation by Stanislas Bach&lt;stanislasbach@gmail.com&gt;</string>
+ <string name="warn_no_dns">"Pas de serveurs DNS utilisés. La résolution de noms peut ne pas fonctionner. Envisagez d\'utiliser des serveurs DNS personnalisés."</string>
+ <string name="dns_add_error">"Impossible d\'ajouter le serveur DNS \"%1$s\", rejetés par le système: %2$s"</string>
+ <string name="getproxy_error">"Erreur d\'obtention des paramètres de proxy: %s"</string>
+ <string name="using_proxy">"Utilisation du proxy %1$s %2$d"</string>
+ <string name="ignore">"Ignorer"</string>
+ <string name="restart">"Redémarrer"</string>
+ <string name="restart_vpn_after_change">"Les changements de configuration sont appliquées après redémarrage du VPN. (Re)démarrer le VPN maintenant?"</string>
+ <string name="configuration_changed">"Configuration modifiée"</string>
+
+</resources>
diff --git a/app/src/main/res/values-id/arrays.xml b/app/src/main/res/values-id/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-id/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml
new file mode 100644
index 00000000..ad893b75
--- /dev/null
+++ b/app/src/main/res/values-id/strings.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Batal</string>
+ <string name="repository_url_text">Kode program dan perekam masalah tersedia di</string>
+ <string name="copyright_others">Aplikasi memakai komponen berikut; lihat kode program untuk lebih jelas mengenai lisensi</string>
+ <string name="about">Tentang...</string>
+ <string name="no_error_found">Tidak ada kesalahan</string>
+ <string name="custom_route_format_error">Gagal menganalisa rute buatan</string>
+ <string name="vpn_launch_title">Hubungkan VPN</string>
+ <string name="shortcut_profile_notfound">Profil di shrotcut tidak ada</string>
+ <string name="route_rejected">Rute ditolak Android</string>
+ <string name="cancel_connection">Putus</string>
+ <string name="clear_log">Bersihkan catatan</string>
+ <string name="title_cancel">Batal Konfirmasi</string>
+ <string name="cancel_connection_query">Putuskan sambungan VPN/Batalkan usaha menyambungkan VPN?</string>
+ <string name="edit_vpn">Ubah seting OpenVPN</string>
+ <string name="tun_error_helpful">Pada beberapa setelan manual gambar ICS izin pada/dev/tun mungkin salah, atau modul tun mungkin hilang sepenuhnya.</string>
+ <string name="tun_open_error">Gagal membuka layanan antarmuka TUN</string>
+ <string name="error">"Kesalahan: "</string>
+ <string name="clear">Bersihkan</string>
+ <string name="info">Info</string>
+ <string name="show_connection_details">Detail koneksi</string>
+ <string name="last_openvpn_tun_config">Konfigurasi terakhir dari OpenVPN:</string>
+ <string name="local_ip_info">IPv4 lokal : %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">Server DNS : %s</string>
+ <string name="dns_domain_info">Domain DNS : %s</string>
+ <string name="routes_info">Rute: %s</string>
+ <string name="routes_info6">Rute IPv6: %s</string>
+ <string name="ip_not_cidr">Memilki informasi antarmuka %1$ s dan %2$, asumsi alamat kedua adalah alamat remote. Menggunakan netmask /32 untuk IP lokal. Mode yang diberikan oleh OpenVPN adalah \"%3$\".</string>
+ <string name="route_not_cidr">Tidak masuk akal membuat %1$ s dan %2$ s sebagai rute IP dengan netmask CIDR, Gunakan /32 sebagai netmask.</string>
+ <string name="route_not_netip">rute yang diperbaiki %1$s/%2$s hingga %3$s/%2$s</string>
+ <string name="keychain_access">Tidak dapat mengakses sertifikat Keychain Android. Dapat disebabkan karena upgrade firmware atau pengembalian backup pengaturan app. Mohon ubah VPN, dan pilih ulang sertifikat berbasis pengaturan dasar agar izin mengakses sertifikat dapat dibuat ulang.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Kirim berkas catatan</string>
+ <string name="send">Kirim</string>
+ <string name="bitmask_openvpn_log_file">Berkas catatan LEAP Android</string>
+ <string name="copied_entry">Salin catatan masuk ke clipboard</string>
+ <string name="no_vpn_support_image">Gambar Anda tidak mendukung VPNService API, maaf:(</string>
+ <string name="opentun_no_ipaddr">TUN tidak dapat dibuka tanpa informasi IP</string>
+ <string name="speed_waiting">Menunggu pesan status…</string>
+ <string name="converted_profile">Profil yang diambil</string>
+ <string name="converted_profile_i">profil yang diambil %d</string>
+ <string name="error_empty_username">Nama pengguna tidak boleh kosong.</string>
+ <string name="pkcs12_file_encryption_key">Berkas kunci enkripsi PKCS12</string>
+ <string name="private_key_password">Sandi kunci pribadi</string>
+ <string name="password">Sandi</string>
+ <string name="building_configration">Membuat konfigurasi…</string>
+ <string name="cert_from_keystore">Sertifikat didapatkan \'%s\' dari Keystore</string>
+ <string name="netstatus">Status jaringan: %s</string>
+ <string name="keychain_nocacert">Tidak ada sertifikat CA yang didapat saat membaca dari Android Keystore. Otentifikasi mungkin gagal</string>
+ <string name="mobile_info">Berjalan di %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Kesalahan masuk dengan kunci Android keystore %1$ s: %2$ s</string>
+ <string name="translationby">Terjemah Bahasa Indonesia oleh Dayro</string>
+ <string name="warn_no_dns">Tidak ada server DNS yang digunakan. Resolusi nama mungkin tidak bisa bekerja. Pertimbangkan seting server DNS yang diatur sendiri </string>
+ <string name="dns_add_error">Tak bisa menambahkan Server DNS \"%1$ s\", ditolak oleh sistem: %2$ s</string>
+ <string name="getproxy_error">Gagal mendapatkan pengaturan proxy: %s</string>
+ <string name="using_proxy">Menggunakan proxy %1$ s %2$ d</string>
+ <string name="ignore">Abaikan</string>
+ <string name="restart">Restart</string>
+ <string name="restart_vpn_after_change">Perubahan konfigurasi baru diterapkan setelah restart VPN. Restart VPN sekarang?</string>
+ <string name="configuration_changed">Konfigurasi berubah</string>
+
+ <string name="minidump_generated">OpenVPN crash tak terduga. Silakan mempertimbangkan mengirim menggunakan pilihan Minidump di Menu Utama</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+
+ <string name="state_connecting">Menghubungkan</string>
+ <string name="state_wait">Menunggu jawaban server</string>
+ <string name="state_auth">Melakukan otentifikasi</string>
+ <string name="state_get_config">Mengambil konfigurasi klien</string>
+ <string name="state_assign_ip">Menetapkan alamat IP</string>
+ <string name="state_add_routes">Menambahkan rute</string>
+ <string name="state_connected">Terhubung</string>
+ <string name="state_reconnecting">Menghubungkan kembali</string>
+ <string name="state_exiting">Keluar</string>
+ <string name="state_noprocess">Tidak berjalan</string>
+ <string name="state_resolve">Mengenali nama host</string>
+ <string name="state_tcp_connect">Menghubungkan (TCP)</string>
+ <string name="state_auth_failed">Otentifikasi gagal</string>
+ <string name="state_nonetwork">Menunggu jaringan yang dapat dipakai</string>
+</resources>
diff --git a/app/src/main/res/values-it/arrays.xml b/app/src/main/res/values-it/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-it/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
new file mode 100644
index 00000000..788dbcc5
--- /dev/null
+++ b/app/src/main/res/values-it/strings.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">Annulla</string>
+ <string name="repository_url_text">Il codice sorgente e il bug tracker sono disponibili all\'indirizzo https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Questo programma usa i seguenti componenti; guarda il codice sorgente per i dettagli completi sulle licenze</string>
+ <string name="about">Informazioni</string>
+ <string name="no_error_found">Nessun errore trovato</string>
+ <string name="config_error_found">Errore nella configurazione</string>
+ <string name="ipv4_format_error">Impossibile analizzare l\'indirizzo IPv4</string>
+ <string name="custom_route_format_error">Errore durante la lettura delle regole di reindirizzamento (routing)</string>
+ <string name="vpn_launch_title">Connetti alla VPN</string>
+ <string name="shortcut_profile_notfound">Profilo indicato nel collegamento non trovato</string>
+ <string name="route_rejected">Reindirizzamento (route) rifiutato da Android</string>
+ <string name="cancel_connection">Scollega</string>
+ <string name="clear_log">Cancella registro</string>
+ <string name="title_cancel">Conferma l\'annullamento</string>
+ <string name="cancel_connection_query">Disconnetti la VPN in uso/annulli il tentativo di connessione?</string>
+ <string name="edit_vpn">Modifica impostazioni VPN</string>
+ <string name="tun_error_helpful">In alcune immagini ICS personalizzate i permessi su /dev/tun potrebbero essere errati, oppure il modulo TUN completamente assente.</string>
+ <string name="tun_open_error">Impossibile accedere all\'interfaccia tun</string>
+ <string name="error">"Errore:"</string>
+ <string name="clear">Azzera</string>
+ <string name="info">Info</string>
+ <string name="show_connection_details">Visualizza i dettagli della connessione</string>
+ <string name="last_openvpn_tun_config">Ultima configurazione interfaccia OpenVPN:</string>
+ <string name="local_ip_info">Indirizzi locali - IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">Server DNS: %s</string>
+ <string name="dns_domain_info">Dominio DNS: %s</string>
+ <string name="routes_info">Instradamenti (route): %s</string>
+ <string name="routes_info6">Instradamenti (route) IPv6: %s</string>
+ <string name="ip_not_cidr">Ottenute le informazioni sulle interfacce %1$s e %2$s, assumendo che il secondo indirizzo sia il peer remoto. Utilizzata la maschera /32 per l\'IP locale. La modalità impostata da OpenVPN è \"%3$s\".</string>
+ <string name="route_not_cidr">Impossibile utilizzare %1$s e %2$s come reindirizzamenti IP con la maschera CIDR, è stata quindi usata la maschera /32.</string>
+ <string name="route_not_netip">Instradamento %1$s/%2$s corretto con %3$s/%2$s</string>
+ <string name="keychain_access">Impossibile accedere ai certificati della Keychain di Android (può essere causato da un aggiornamento del firmware o dal ripristino di un backup dell\'applicazione o delle sue impostazioni). E\' necessario modificare le impostazioni della VPN e riselezionare il certificato nelle impostazioni principali per ricreare i permessi di accesso al certificato.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Invia il file di log</string>
+ <string name="send">Invia</string>
+ <string name="bitmask_openvpn_log_file">File log di LEAP Android</string>
+ <string name="copied_entry">Voce di registro copiata negli appunti</string>
+ <string name="no_vpn_support_image">La tua immagine non è supportata dal VPNService API, mi dispiace :(</string>
+ <string name="opentun_no_ipaddr">Rifiuto di attivare il dispositivo tun senza informazioni sull\'IP</string>
+ <string name="speed_waiting">In attesa del messaggio di stato...</string>
+ <string name="converted_profile">profilo importato</string>
+ <string name="converted_profile_i">profilo importato %d</string>
+ <string name="error_empty_username">L\'username non deve essere vuoto.</string>
+ <string name="pkcs12_file_encryption_key">File con la chiave di crittografia PKCS12</string>
+ <string name="private_key_password">Password della chiave privata</string>
+ <string name="password">Password</string>
+ <string name="building_configration">Configurazione in corso...</string>
+ <string name="cert_from_keystore">Ottenuto il certificato \'%s\' dal Keystore</string>
+ <string name="netstatus">Stato della rete: %s</string>
+ <string name="keychain_nocacert">Nessun certificato della CA è stato prelevato dal Keystore di Android. E\' probabile che l\'autenticazione fallisca.</string>
+ <string name="mobile_info">In esecuzione su %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Errore di firma con la chiave %1$s: %2$s del Keystore di Android.</string>
+ <string name="translationby">Traduzione in inglese di Arne Schwabe&lt;arne@rfc2549.org&gt;</string>
+ <string name="warn_no_dns">Nessun server DNS in uso. La risoluzione dei nomi potrebbe non funzionare. Valuta se inserire dei server DNS personalizzati.</string>
+ <string name="dns_add_error">Impossibile aggiungere il server DNS \"%1$s\", respinto dal sistema: %2$s</string>
+ <string name="getproxy_error">Errore nell\'ottenere le impostazioni del proxy: %s</string>
+ <string name="using_proxy">Si sta utilizzando il proxy %1$s %2$d</string>
+ <string name="ignore">Ignora</string>
+ <string name="restart">Riavvia</string>
+ <string name="restart_vpn_after_change">Le modifiche sarannoi applicate dopo aver riavviato la connessione VPN. Riavviare ora la connessione?</string>
+ <string name="configuration_changed">Configurazione modificata</string>
+
+ <string name="minidump_generated">OpenVPN si è arrestato in modo imprevisto. Ti consigliamo di attivare l\'opzione Invia Minidump nel menu principale.</string>
+</resources>
diff --git a/app/src/main/res/values-ja/arrays.xml b/app/src/main/res/values-ja/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-ja/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
new file mode 100644
index 00000000..4503c227
--- /dev/null
+++ b/app/src/main/res/values-ja/strings.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="cancel">キャンセル</string>
+ <string name="repository_url_text">ソースコードと問題管理は以下で: https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">プログラムは、次のコンポーネントを使用します。完全な詳細についてはソース上のライセンスを参照してください。</string>
+ <string name="about">バージョン情報</string>
+ <string name="no_error_found">エラーは見つかりませんでした。</string>
+ <string name="config_error_found">設定に誤りがあります。</string>
+ <string name="ipv4_format_error">IPv4 アドレスの解析エラー</string>
+ <string name="custom_route_format_error">カスタム経路の解析エラー</string>
+ <string name="vpn_launch_title">VPNに接続</string>
+ <string name="shortcut_profile_notfound">ショートカットで指定されたプロファイルが見つかりません</string>
+ <string name="route_rejected">経路がAndroidにより拒否されました。</string>
+ <string name="cancel_connection">切断</string>
+ <string name="clear_log">ログをクリア</string>
+ <string name="title_cancel">キャンセルの確認</string>
+ <string name="cancel_connection_query">接続中または試行中の接続をキャンセルしますか?</string>
+ <string name="edit_vpn">VPN 設定の編集</string>
+ <string name="tun_error_helpful">いくつかのカスタムICSイメージは、/dev/tunのパーミッションが異常か、TUNモジュールが含まれていません。</string>
+ <string name="tun_open_error">TUNデバイスを開こうとして失敗しました。</string>
+ <string name="error">"エラー:"</string>
+ <string name="clear">クリア</string>
+ <string name="info">情報</string>
+ <string name="show_connection_details">接続の詳細を表示</string>
+ <string name="last_openvpn_tun_config">OpenVPNから設定した最後のインターフェイス:</string>
+ <string name="local_ip_info">ローカル IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS サーバー: %s</string>
+ <string name="dns_domain_info">DNS ドメイン: %s</string>
+ <string name="routes_info">経路:%s</string>
+ <string name="routes_info6">経路 IPv6:%s</string>
+ <string name="ip_not_cidr">インターフェース情報として[%1$s]と[%2$s]を取得しました。2つめのアドレスはリモート側のピアアドレスです。32ビットマスクをローカルIPに使用します。 OpenVPNのモードは[%3$s]です。</string>
+ <string name="route_not_cidr">%1$sと%2$sではCIDR形式のIP経路情報として意味をなしません。32ビットマスクを使用します。</string>
+ <string name="route_not_netip">経路情報%1$s/%2$sを%3$s/%2$sに修正しました。</string>
+ <string name="keychain_access">Androidの証明書管理にアクセスできません。(ファームウェアの更新、アプリケーションまたはその設定のリストアによって発生する場合があります)。VPNの設定で証明書の選択を再度行ってください。</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">ログ ファイルを送信します。</string>
+ <string name="send">送信</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android ログ ファイル</string>
+ <string name="copied_entry">クリップ ボードにコピーされたログ エントリ</string>
+ <string name="no_vpn_support_image">申し訳ありませんが、お使いの環境ではVPNサービスがサポートされていません。</string>
+ <string name="opentun_no_ipaddr">IP情報なしでのTUNデバイス使用は拒否しています</string>
+ <string name="speed_waiting">状態メッセージを待っています。</string>
+ <string name="converted_profile">インポートされたプロファイル</string>
+ <string name="converted_profile_i">インポートされたプロファイル %d</string>
+ <string name="error_empty_username">ユーザ名を空に設定することはできません</string>
+ <string name="pkcs12_file_encryption_key">PKCS12ファイルの暗号化キー</string>
+ <string name="private_key_password">秘密鍵のパスワード</string>
+ <string name="password">パスワード</string>
+ <string name="building_configration">構成中・・・</string>
+ <string name="cert_from_keystore">\'%s\'の証明書をキーストアから読み出し</string>
+ <string name="netstatus">ネットワーク状態: %s</string>
+ <string name="keychain_nocacert">認証局証明書(CA Cert)がAndroidのキーストアから取得できませんでした。認証はおそらく失敗します。</string>
+ <string name="mobile_info">実行中:%1$s (%2$s)%3$s Android API %4$d</string>
+ <string name="error_rsa_sign">Androidキーストアに保存されたキー %1$s: %2$sの署名エラーです</string>
+ <string name="translationby">日本語翻訳 高橋正希@埼玉 &lt;tools@artin.nu&gt;</string>
+ <string name="warn_no_dns">使用されている DNS サーバーはありません。名前解決は動作しません。DNSサーバーの設定を見直してください。</string>
+ <string name="dns_add_error">DNSサーバ \"%1$s\" の追加に失敗しました。%2$sに拒否されました。</string>
+ <string name="getproxy_error">プロキシ設定でエラー: %s</string>
+ <string name="using_proxy">プロキシを使用します %1$s %2$d</string>
+ <string name="ignore">無視</string>
+ <string name="restart">再起動</string>
+ <string name="restart_vpn_after_change">設定の変更はVPNの再起動後に反映されます。VPNを(再)起動しますか?</string>
+ <string name="configuration_changed">設定が変更されました</string>
+
+ <string name="minidump_generated">OpenVPN は予期せず終了しました。メイン メニューでミニダンプの送信オプションを検討してください。</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+
+ <string name="state_connecting">接続中</string>
+ <string name="state_wait">サーバーの応答を待っています。</string>
+ <string name="state_auth">認証中</string>
+ <string name="state_get_config">クライアントの構成を取得中</string>
+ <string name="state_assign_ip">IPアドレスを割り当て中</string>
+ <string name="state_add_routes">経路を追加中</string>
+ <string name="state_connected">接続しました</string>
+ <string name="state_reconnecting">再接続中</string>
+ <string name="state_exiting">終了中</string>
+ <string name="state_noprocess">停止中</string>
+ <string name="state_resolve">ホスト名を解決中</string>
+ <string name="state_tcp_connect">接続中(TCP)</string>
+ <string name="state_auth_failed">認証に失敗しました</string>
+ <string name="state_nonetwork">使用可能なネットワークを待機中</string>
+</resources>
diff --git a/app/src/main/res/values-ko/arrays.xml b/app/src/main/res/values-ko/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-ko/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
new file mode 100644
index 00000000..8a725bad
--- /dev/null
+++ b/app/src/main/res/values-ko/strings.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="repository_url_text">소스 코드와 문제 추적기는 https://github.com/leapcode/bitmask_android/에서 사용할 수 있습니다</string>
+ <string name="copyright_others">프로그램은 다음 구성 요소를 사용합니다. 라이선스에 대 한 자세한 내용은 소스를 참조 하십시오</string>
+ <string name="about">소개</string>
+ <string name="no_error_found">오류 없음</string>
+ <string name="config_error_found">설정 오류</string>
+ <string name="ipv4_format_error">IPv4 주소 구문 분석 오류</string>
+ <string name="custom_route_format_error">사용자 지정 경로 구문 분석 오류</string>
+ <string name="vpn_launch_title">VPN에 연결 하기</string>
+ <string name="shortcut_profile_notfound">바로 가기에 지정 된 프로파일을 찾을 수 없습니다.</string>
+ <string name="route_rejected">안드로이드에 의해 거부된 라우트</string>
+ <string name="cancel_connection">연결 끊기</string>
+ <string name="clear_log">로그 지우기</string>
+ <string name="title_cancel">취소 확인</string>
+ <string name="cancel_connection_query">연결된 VPN 끊기/연결시도 취소?</string>
+ <string name="edit_vpn">VPN 설정 편집</string>
+ <string name="tun_error_helpful">일부 사용자 지정 ICS 이미지에서는 /dev/tun에 대한 권한이 잘못 되어 있거나 tun 모듈 자체가 누락 될 수 있습니다.</string>
+ <string name="tun_open_error">Tun 인터페이스를 열지 못했습니다</string>
+ <string name="error">"오류:"</string>
+ <string name="clear">지우기</string>
+ <string name="info">정보</string>
+ <string name="show_connection_details">연결 세부 정보 보기</string>
+ <string name="last_openvpn_tun_config">Openvpn에서 마지막 인터페이스 구성:</string>
+ <string name="local_ip_info">로컬 IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS 서버: %s</string>
+ <string name="dns_domain_info">DNS 도매인: %s</string>
+ <string name="routes_info">라우트: %s</string>
+ <string name="routes_info6">라우트 IPv6: %s</string>
+ <string name="ip_not_cidr">인터페이스 정보 %1$s와 %2$s에 있어, 두 번째 주소를 원격 피어 주소로 가정 하겠습니다. 로컬 IP의 넷마스크로는 /32를 사용하겠습니다. OpenVPN에 의해 주어진 모드는 \"%3$s\" 입니다.</string>
+ <string name="route_not_cidr">CIDR 넷마스크가 있는 IP 라우트 %1$s 와 %2$s 에 있어서 처리가 불가능합니다. /32를 넷마스크로 사용하겠습니다.</string>
+ <string name="route_not_netip">%1$s/%2$s 에서 %3$s/%2$s로 라우트 수정</string>
+ <string name="keychain_access">안드로이드 키체인 인증서에 접근할 수 없습니다. 펌웨어 업그래이드 또는 백업된 앱/앱 설정을 복구하면서 발생할 수 있습니다. 인증서에 액세스할 수 있는 권한을 다시 생성하기 위해 VPN을 편집 하고 기본 설정 아래에서 인증서를 다시 선택 하십시오.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">로그 파일 보내기</string>
+ <string name="send">보내기</string>
+ <string name="bitmask_openvpn_log_file">LEAP 로그 파일</string>
+ <string name="copied_entry">클립보드로 로그 복사</string>
+ <string name="no_vpn_support_image">당신의 이미지는 VPNService API를 지원 하지 않습니다, 죄송 합니다:(</string>
+ <string name="opentun_no_ipaddr">IP 정보가 없는 tun 장치 열기를 거부합니다</string>
+ <string name="speed_waiting">상태 메시지를 기다리는 중...</string>
+ <string name="converted_profile">가져온 프로파일</string>
+ <string name="converted_profile_i">가져온 프로파일 %d</string>
+ <string name="error_empty_username">사용자 이름이 비어 있지 않아야 합니다.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 파일 암호화 키</string>
+ <string name="private_key_password">개인 키 암호</string>
+ <string name="password">암호</string>
+ <string name="building_configration">설정 만드는중…</string>
+ <string name="cert_from_keystore">Keystore에서 인증서 \'%s\' 받음</string>
+ <string name="netstatus">네트워크 상태: %s</string>
+ <string name="keychain_nocacert">안드로이드 keystore에서 CA 인증서를 찾지 못했습니다. Auhtentication은 실패할 것 입니다.</string>
+ <string name="mobile_info">%1$s (%2$s) %3$s, 안드로이드 API %4$d 에서 실행</string>
+ <string name="error_rsa_sign">안드로이드 keystore 키 %1$s: %2$s과 싸이닝 오류</string>
+ <string name="translationby">한국어 번역 (주)기가드 안규태&lt;ktdann@gmail.com></string>
+ <string name="warn_no_dns">사용 중인 DNS 서버가 없습니다. 이름 확인이 작동 하지 않을 수 있습니다. 사용자 지정 DNS 서버의 사용을 고려해 보세요.</string>
+ <string name="dns_add_error">DNS서버 \"%1$s\" 는, 시스템에 의해 거부되 추가할 수 없습니다: %2$s</string>
+ <string name="getproxy_error">프록시 설정 가져오기 오류: %s</string>
+ <string name="using_proxy">프록시 %1$s %2$d 을 사용</string>
+ <string name="ignore">무시</string>
+ <string name="restart">다시 시작</string>
+ <string name="restart_vpn_after_change">VPN을 다시 시작한 후 설정 변경 내용이 적용 됩니다. VPN을 지금 (재)시작?</string>
+ <string name="configuration_changed">설정 변경</string>
+
+ <string name="minidump_generated">OpenVPN이 예기치 않게 종료됐습니다. 메인 메뉴에 있는 미니 덤프 보내기 옵션의 사용을 고려 하시기 바랍니다</string>
+</resources>
diff --git a/app/src/main/res/values-nl/arrays.xml b/app/src/main/res/values-nl/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-nl/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
new file mode 100644
index 00000000..0442b054
--- /dev/null
+++ b/app/src/main/res/values-nl/strings.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="about">Over</string>
+ <string name="no_error_found">Geen fout.</string>
+ <string name="config_error_found">Fout in de configuratie</string>
+ <string name="vpn_launch_title">Met VPN verbinden</string>
+ <string name="route_rejected">Route geweigert door Android</string>
+ <string name="cancel_connection">Verbinding verbreken</string>
+ <string name="clear_log">logboek wissen</string>
+ <string name="title_cancel">Annuleer bevestiging</string>
+ <string name="cancel_connection_query">Sluit de verbonden VPN af/annuleer de verbindingspoging?</string>
+ <string name="edit_vpn">VPN Instellingen Bewerken</string>
+ <string name="error">"Fout:"</string>
+ <string name="clear">Leeg maken</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Details van de verbinding weergeven</string>
+ <string name="last_openvpn_tun_config">Laatste interfaceconfiguratie van OpenVPN:</string>
+ <string name="local_ip_info">Lokaal IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS Server: %s</string>
+ <string name="dns_domain_info">DNS Domein: %s</string>
+ <string name="routes_info">Routes: %s</string>
+ <string name="routes_info6">Routes IPv6: %s</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Logboek verzenden</string>
+ <string name="send">Verzenden</string>
+ <string name="speed_waiting">Wachten op status bericht…</string>
+ <string name="converted_profile">Geïmporteerd profiel</string>
+ <string name="converted_profile_i">Geïmporteerd profiel %d</string>
+ <string name="error_empty_username">De gebruikersnaam moet niet leeg zijn.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 Bestand Encryptie Sleutel</string>
+ <string name="private_key_password">Privé Sleutel Wachtwoord</string>
+ <string name="password">Wachtwoord</string>
+</resources>
diff --git a/app/src/main/res/values-no/arrays.xml b/app/src/main/res/values-no/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-no/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml
new file mode 100644
index 00000000..a363b2f9
--- /dev/null
+++ b/app/src/main/res/values-no/strings.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="about">Om</string>
+ <string name="no_error_found">Ingen feil funnet</string>
+ <string name="config_error_found">Feil i konfigurasjonen</string>
+ <string name="vpn_launch_title">Koble til VPN</string>
+ <string name="cancel_connection">Koble fra</string>
+ <string name="clear_log">Tøm logg</string>
+ <string name="title_cancel">Avbryt bekreftelse</string>
+ <string name="edit_vpn">Rediger VPN-innstillinger</string>
+ <string name="error">"Feil:"</string>
+ <string name="clear">Fjern</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Vis Tilkoblingsdetaljer</string>
+ <string name="dns_server_info">DNS-server: %s</string>
+ <string name="dns_domain_info">DNS-domene: %s</string>
+ <string name="routes_info">Ruter: %s</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Send loggfilen</string>
+ <string name="send">Send</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android loggfil</string>
+ <string name="speed_waiting">Venter på tilstands melding...</string>
+ <string name="converted_profile">importert profil</string>
+ <string name="converted_profile_i">importert profilen %d</string>
+ <string name="error_empty_username">Brukernavnet kan ikke være tomt.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 Filkrypteringsnøkkel</string>
+ <string name="private_key_password">Privat nøkkel passord</string>
+ <string name="password">Passord</string>
+ <string name="building_configration">Lager konfigurasjon...</string>
+ <string name="netstatus">Nettverksstatus: %s</string>
+ <string name="mobile_info">Kjører på %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="translationby">Norsk oversettelse av Jonny</string>
+ <string name="getproxy_error">Feil ved henting av proxy-innstillinger: %s</string>
+ <string name="using_proxy">Bruker proxy %1$s %2$d</string>
+ <string name="ignore">Ignorer</string>
+ <string name="restart">Start på nytt</string>
+ <string name="configuration_changed">Konfigurasjon endret</string>
+</resources>
diff --git a/app/src/main/res/values-ro/arrays.xml b/app/src/main/res/values-ro/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-ro/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml
new file mode 100644
index 00000000..22496320
--- /dev/null
+++ b/app/src/main/res/values-ro/strings.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="repository_url_text">Cod sursă şi tracker probleme disponibile la https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Acest program utilizează următoarele componente; a se vedea codul sursă pentru mai multe detalii despre licente</string>
+ <string name="about">Despre</string>
+ <string name="no_error_found">Nu s-a găsit nici o eroare</string>
+ <string name="config_error_found">Eroare în configurare</string>
+ <string name="ipv4_format_error">Eroare parsare adresă IPv4</string>
+ <string name="custom_route_format_error">Eroare parsare rute particularizate</string>
+ <string name="vpn_launch_title">Conectare la VPN</string>
+ <string name="shortcut_profile_notfound">Profilul specificat în comanda rapidă nu a fost găsit</string>
+ <string name="route_rejected">Rută respinsă de Android</string>
+ <string name="cancel_connection">Deconectaţi</string>
+ <string name="clear_log">Golire jurnal</string>
+ <string name="title_cancel">Anulare confirmare</string>
+ <string name="cancel_connection_query">Deconectaţi VPN-ul conectat/anulaţi încercarea de conectare?</string>
+ <string name="edit_vpn">Editaţi setările VPN</string>
+ <string name="tun_error_helpful">În cazul anumitor imagini particularizate de ICS permisiile pentru /dev/tun pot fi greşite sau modulul tun lipseşte cu desăvârşire.</string>
+ <string name="tun_open_error">Eroare deschidere interfaţa tun</string>
+ <string name="error">"Eroare:"</string>
+ <string name="clear">Goleşte</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Arată detaliile conexiunii</string>
+ <string name="last_openvpn_tun_config">Ultima configurare interfaţă din OpenVPN:</string>
+ <string name="local_ip_info">Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">Server DNS: %s</string>
+ <string name="dns_domain_info">Domeniu DNS: %s</string>
+ <string name="routes_info">Rute: %s</string>
+ <string name="routes_info6">Rute IPv6: %s</string>
+ <string name="ip_not_cidr">S-au primit informaţiile despre interfaţă %1$s şi %2$s, presupun că a doua adresă este adresa peer a serverlui remote. Folosesc netmask /32 pentru IP local. Modul dat de OpenVPN este \"%3$s\".</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Trimite fişier jurnal</string>
+ <string name="send">Trimite</string>
+ <string name="bitmask_openvpn_log_file">Fişier jurnal LEAP Android</string>
+ <string name="converted_profile">profil importat</string>
+ <string name="converted_profile_i">profil importat %d</string>
+ <string name="pkcs12_file_encryption_key">Cheie criptare fişier PKCS12</string>
+ <string name="private_key_password">Parola cheie privată</string>
+ <string name="password">Parola</string>
+ <string name="building_configration">Se generează configurarea…</string>
+ <string name="cert_from_keystore">Am primit certificatul \'%s\' din Keystore</string>
+ <string name="netstatus">Statutus reţea: %s</string>
+ <string name="mobile_info">Rulează pe %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Eroare semnare cu Android keystore key %1$s: %2$s</string>
+ <string name="using_proxy">Folosesc proxy %1$ s %2$ d</string>
+ <string name="ignore">Ignora</string>
+ <string name="restart">Restart</string>
+ <string name="configuration_changed">Configuraţie schimbată</string>
+
+ <string name="minidump_generated">OpenVPN sa oprit în mod neaşteptat. Vă rugăm să consideraţi opţiunea de trimitere a unui Minidump din meniul principal</string>
+</resources>
diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-ru/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..aaa42690
--- /dev/null
+++ b/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="copyright_others">Данная программа использует следующие компоненты; смотрите исходный код для получения подробной информации о лицензии</string>
+ <string name="about">О программе</string>
+ <string name="no_error_found">Ошибок не найдено</string>
+ <string name="config_error_found">Ошибка в конфигурации</string>
+ <string name="ipv4_format_error">Невозможно прочесть IPv4 адрес</string>
+ <string name="custom_route_format_error">Невозможно примениить пользовательские маршруты</string>
+ <string name="vpn_launch_title">Подключиться к VPN</string>
+ <string name="shortcut_profile_notfound">Не найден профиль, указанный в ярлыке</string>
+ <string name="route_rejected">Маршрут отвергнут Android</string>
+ <string name="cancel_connection">Отключение</string>
+ <string name="clear_log">очистить журнал</string>
+ <string name="title_cancel">Подтверждение отмены</string>
+ <string name="cancel_connection_query">Отключение активных VPN/Отмена попыток подключения?</string>
+ <string name="edit_vpn">Редактирование параметров VPN</string>
+ <string name="tun_error_helpful">На некторых костомных сборках права на /dev/tun могут быть неверными или tun-модуль может быть не включен. Для прошивки CM9 можете попробовать исправить владельца прямо из настроек программы</string>
+ <string name="tun_open_error">Открытие интерфейса tun окончилось неудачей</string>
+ <string name="error">"Ошибка: "</string>
+ <string name="clear">Очистить</string>
+ <string name="info">информация</string>
+ <string name="show_connection_details">Показать подробности о подключении</string>
+ <string name="last_openvpn_tun_config">Последняя используемая конфигурация OpenVPN:</string>
+ <string name="local_ip_info">Адрес IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS-сервер: %s</string>
+ <string name="dns_domain_info">DNS-домен: %s</string>
+ <string name="routes_info">Маршруты: %s</string>
+ <string name="routes_info6">Маршруты IPv6: %s</string>
+ <string name="ip_not_cidr">Получена информация интерфейса %1$s и %2$s, второй адрес является удалённым адресом канала. Используется сетевая маска /32 для локального IP адреса. Режим, установленный OpenVPN: \"%3$s\".</string>
+ <string name="route_not_cidr">Невозможно использовать выражения %1$s и %2$s как маршрут по стандарту CIDR. используется /32 как маска подсети.</string>
+ <string name="route_not_netip">Маршрут исправлен с %1$s/%2$s на %3$s/%2$s</string>
+ <string name="keychain_access">Не удается получить доступ к хранилищу ключей и сертификатов Android. Это может быть вызвано обновлением прошивки или восстановления старой копии приложения или его настроек. Пожалуйста, отредактируйте профиль VPN и заново укажите ключи и сертификаты в разделе Основные параметры.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Отправить файл журнала</string>
+ <string name="send">Отправить</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android файла лога</string>
+ <string name="copied_entry">Скопировать лог в буфер обмена</string>
+ <string name="no_vpn_support_image">Ваша прошивка не поддерживает VPNService API, извините :(</string>
+ <string name="opentun_no_ipaddr">Отказ в открытии устройства tun без информации об IP-адресе</string>
+ <string name="speed_waiting">Ожидание сообщения о состоянии…</string>
+ <string name="converted_profile">импортируемый профиль</string>
+ <string name="converted_profile_i">импортируемый профиль %d</string>
+ <string name="error_empty_username">Имя пользователя не должно быть пустым.</string>
+ <string name="pkcs12_file_encryption_key">Файл PKCS12-ключа</string>
+ <string name="private_key_password">Пароль закрытого ключа</string>
+ <string name="password">Пароль</string>
+ <string name="building_configration">Создание конфигурации…</string>
+ <string name="cert_from_keystore">Получен сертификат \'%s\' из хранилища ключей</string>
+ <string name="netstatus">Статус сети: %s</string>
+ <string name="keychain_nocacert">Не удалось получить CA из хранилища ключей Android. Аутентификация не удалась.</string>
+ <string name="mobile_info">Работает на %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Ошибка подписи с использованием ключа из хранилища Android %1$s: %2$s</string>
+ <string name="translationby">Русский перевод от RusFox &lt;horonitel@gmail.com&gt;</string>
+ <string name="warn_no_dns">DNS-серверы не используются. Разрешение имен может не работать. Рассмотрите возможность указания DNS-серверов</string>
+ <string name="dns_add_error">Не удалось добавить DNS-сервер \"%1$s\", отклонен системой: %2$s</string>
+ <string name="getproxy_error">Ошибка при получении параметров прокси-сервера: %s</string>
+ <string name="using_proxy">Используется прокси-сервер %1$s %2$d</string>
+ <string name="ignore">Игнорировать</string>
+ <string name="restart">Перезагрузка</string>
+ <string name="restart_vpn_after_change">Изменения конфигурации применяются после перезапуска VPN. (Пере)запустить VPN теперь?</string>
+ <string name="configuration_changed">Конфигурация изменена</string>
+
+ <string name="minidump_generated">OpenVPN завершилась неожиданно. Пожалуйста, посмотрите опцию \"Отправить Minidump\" в главном меню</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+
+ <string name="state_connecting">Подключение</string>
+ <string name="state_wait">Ожидание ответа сервера</string>
+ <string name="state_auth">Проверка подлинности</string>
+ <string name="state_get_config">Получение конфигурации клиента</string>
+ <string name="state_assign_ip">Назначение IP-адресов</string>
+ <string name="state_add_routes">Добавление маршрутов</string>
+ <string name="state_connected">Подключено</string>
+ <string name="state_reconnecting">Повторное подключение</string>
+ <string name="state_exiting">Выход</string>
+ <string name="state_noprocess">Не запущено</string>
+ <string name="state_resolve">Разрешение имен узлов</string>
+ <string name="state_tcp_connect">Подключение (TCP)</string>
+ <string name="state_auth_failed">Ошибка аутентификации</string>
+ <string name="state_nonetwork">Ожидание работы сети</string>
+</resources>
diff --git a/app/src/main/res/values-uk/arrays.xml b/app/src/main/res/values-uk/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-uk/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
new file mode 100644
index 00000000..dab28b48
--- /dev/null
+++ b/app/src/main/res/values-uk/strings.xml
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="repository_url_text">Початковий код і відстеження проблем доступні по https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">Ця програма використовує такі компоненти; перегляньте вихідний код для повної інформації про ліцензії</string>
+ <string name="about">Про</string>
+ <string name="no_error_found">Помилок не знайдено</string>
+ <string name="config_error_found">Помилка конфігурації</string>
+ <string name="ipv4_format_error">Помилка при розборі адреси IPv4</string>
+ <string name="custom_route_format_error">Помилка аналізу налаштованих маршрутів</string>
+ <string name="vpn_launch_title">Підключення до VPN</string>
+ <string name="shortcut_profile_notfound">Профіль, вказаний у ярлику, не знайдено</string>
+ <string name="route_rejected">Маршрут відхилено Андроїдом</string>
+ <string name="cancel_connection">Від\'єднати</string>
+ <string name="clear_log">очистити журнал</string>
+ <string name="title_cancel">Підтвердження скасування</string>
+ <string name="cancel_connection_query">Відключення активних VPN/скасувати спробу підключення?</string>
+ <string name="edit_vpn">Змінити налаштування VPN</string>
+ <string name="tun_error_helpful">На деяких користувацьких прошивках ICS права на /dev/tun можуть бути невірними або модуль tun може бути взагалі відсутнім.</string>
+ <string name="tun_open_error">Не вдалося відкрити інтерфейс tun</string>
+ <string name="error">"Помилка: "</string>
+ <string name="clear">Очистити</string>
+ <string name="info">Інформація</string>
+ <string name="show_connection_details">Показати відомості про підключення</string>
+ <string name="last_openvpn_tun_config">Останній інтерфейс конфігурації з OpenVPN:</string>
+ <string name="local_ip_info">Адреса IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS-сервер: %s</string>
+ <string name="dns_domain_info">DNS домен: %s</string>
+ <string name="routes_info">Маршрути: %s</string>
+ <string name="routes_info6">Маршрути IPv6: %s</string>
+ <string name="ip_not_cidr">Отримано інформацію інтерфейсу %1$s і %2$s, друга адреса є віддаленою адресою каналу. Використовується мережева маска /32 для локальної IP-адреси. Режим, встановлений OpenVPN: \"%3$s\".</string>
+ <string name="route_not_cidr">Неможливо використовувати вирази %1$s і %2$s як маршрут за стандартом CIDR. використовується /32 як маска підмережі.</string>
+ <string name="route_not_netip">Виправлено маршрут з %1$s/%2$s на %3$s/%2$s</string>
+ <string name="keychain_access">Не можна отримати доступ до сховища ключів та сертифікатів Андроїд. Це можливо спричинено оновлення прошивки або відновленням резервної копії програми чи її налаштувань. Будь ласка, відредагуйте профіль VPN та заново виберіть сертифікат у основних параметрах для створення доступу до сертифікату.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Надіслати файл журналу</string>
+ <string name="send">Надіслати</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android файл журналу</string>
+ <string name="copied_entry">Скопійовано запис журналу до буферу обміну</string>
+ <string name="no_vpn_support_image">Ваша прошивка не підтримує VPNService API, вибачте :(</string>
+ <string name="opentun_no_ipaddr">Відмова у відкритті пристрою tun без інформації про IP-адресу</string>
+ <string name="speed_waiting">Очікування повідомлення стану...</string>
+ <string name="converted_profile">імпортований профіль</string>
+ <string name="converted_profile_i">імпортований профіль: %d</string>
+ <string name="error_empty_username">Ім\'я користувача не може бути порожнім.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 Ключ шифрування файлу</string>
+ <string name="private_key_password">Пароль закритого ключа</string>
+ <string name="password">Пароль</string>
+ <string name="building_configration">Побудова конфігурації…</string>
+ <string name="cert_from_keystore">Отримано сертифікат \'%s\' з сховища ключів</string>
+ <string name="netstatus">Статус мережі: %s</string>
+ <string name="keychain_nocacert">Не вдалося отримати СА сертифікат при читанні із сховища ключів Андроїд. Автентифікація не вдалася.</string>
+ <string name="mobile_info">Працює на %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Помилка підпису з використанням ключа із сховища Андроїд %1$s: %2$s</string>
+ <string name="translationby">Переклад українською від wvolov</string>
+ <string name="warn_no_dns">Жодний DNS сервер не використовується. Розширення імен можуть не працювати. Розгляньте можливість вказання DNS серверів</string>
+ <string name="dns_add_error">Не можливо додати DNS-сервер \"%1$s\", відхилено системою: %2$s</string>
+ <string name="getproxy_error">Помилка отримання параметрів проксі: %s</string>
+ <string name="using_proxy">Використовується проксі %1$s %2$d</string>
+ <string name="ignore">Ігнорувати</string>
+ <string name="restart">Перезапустити</string>
+ <string name="restart_vpn_after_change">Після перезапуску VPN застосувати зміни конфігурації. (Пере)запустити VPN тепер?</string>
+ <string name="configuration_changed">Конфігурація змінена</string>
+
+ <string name="minidump_generated">OpenVPN впав несподівано. Будь ласка, розгляньте використання параметру \"Надіслати Мінідамп\" в головному меню</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+
+ <string name="state_connecting">Підключення</string>
+ <string name="state_wait">Очікування відповіді сервера</string>
+ <string name="state_auth">Аутентифікація</string>
+ <string name="state_get_config">Отримання конфігурації клієнта</string>
+ <string name="state_assign_ip">Перепризначення IP-адрес</string>
+ <string name="state_add_routes">Додавання маршрутів</string>
+ <string name="state_connected">Підключено</string>
+ <string name="state_reconnecting">Повторне підключення</string>
+ <string name="state_exiting">Виходимо</string>
+ <string name="state_noprocess">Не працює</string>
+ <string name="state_resolve">Розпізнавання імен вузлів</string>
+ <string name="state_tcp_connect">Підключення (TCP)</string>
+ <string name="state_auth_failed">Помилка автентифікації</string>
+ <string name="state_nonetwork">Очікування на використання мережі</string>
+</resources>
diff --git a/app/src/main/res/values-zh-rCN/arrays.xml b/app/src/main/res/values-zh-rCN/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
new file mode 100644
index 00000000..ae57d277
--- /dev/null
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="repository_url_text">请前往 https://github.com/leapcode/bitmask_android/ 源码或提供问题反馈</string>
+ <string name="copyright_others">本程序使用以下组件,请在 Licenses 查看源码获取更详细内容。</string>
+ <string name="about">关于</string>
+ <string name="no_error_found">未找到错误</string>
+ <string name="config_error_found">配置有错误</string>
+ <string name="ipv4_format_error">无法解析 IPv4 地址</string>
+ <string name="custom_route_format_error">无法解析自定义路由</string>
+ <string name="vpn_launch_title">连接到 VPN</string>
+ <string name="shortcut_profile_notfound">未找到快捷方式中指定的配置文件</string>
+ <string name="route_rejected">Android 拒绝了路由</string>
+ <string name="cancel_connection">断开</string>
+ <string name="clear_log">清除日志</string>
+ <string name="title_cancel">取消确认</string>
+ <string name="cancel_connection_query">断开已连接的 VPN / 取消连接尝试?</string>
+ <string name="edit_vpn">编辑 VPN 设置</string>
+ <string name="tun_open_error">未能打开 tun 模块</string>
+ <string name="error">错误</string>
+ <string name="clear">清除</string>
+ <string name="info">信息</string>
+ <string name="show_connection_details">显示连接信息</string>
+ <string name="last_openvpn_tun_config">最后 OpenVPN 接口配置:</string>
+ <string name="local_ip_info">本地 IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS 服务器: %s</string>
+ <string name="dns_domain_info">DNS 域: %s</string>
+ <string name="routes_info">IPv4 路由: %s</string>
+ <string name="routes_info6">IPv6 路由: %s</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">发送日志文件</string>
+ <string name="send">发送</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android 日志文件</string>
+ <string name="copied_entry">日志条目已复制剪贴板</string>
+ <string name="opentun_no_ipaddr">无 IP 信息,拒绝打开 tun 设备</string>
+ <string name="speed_waiting">等待状态消息</string>
+ <string name="converted_profile">已导入配置文件</string>
+ <string name="converted_profile_i">已导入配置文件 %d</string>
+ <string name="error_empty_username">用户名不能为空。</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 文件加密密钥</string>
+ <string name="private_key_password">私钥密码</string>
+ <string name="password">密码</string>
+ <string name="building_configration">正在生成配置</string>
+ <string name="netstatus">网络状态: %s</string>
+ <string name="mobile_info">运行 %1$s ( %2$s ) %3$s ,Android API %4$d</string>
+ <string name="translationby">中文翻译: 白达卫
+&lt;59539051+ovpntrans.zh@mail.dcu.ie&gt;</string>
+ <string name="getproxy_error">获取代理设置时出错:%s</string>
+ <string name="using_proxy">使用代理 %1$s %2$d</string>
+ <string name="ignore">忽略</string>
+ <string name="restart">重启</string>
+ <string name="configuration_changed">配置已更改</string>
+</resources>
diff --git a/app/src/main/res/values-zh-rTW/arrays.xml b/app/src/main/res/values-zh-rTW/arrays.xml
new file mode 100644
index 00000000..045e125f
--- /dev/null
+++ b/app/src/main/res/values-zh-rTW/arrays.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+</resources>
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 00000000..880fb4ad
--- /dev/null
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="repository_url_text">取得原始碼與個案追蹤,可上 https://github.com/leapcode/bitmask_android/</string>
+ <string name="copyright_others">本程序使用了以下元件,其作者和授權資訊如下</string>
+ <string name="about">關於</string>
+ <string name="no_error_found">未有找到錯誤</string>
+ <string name="config_error_found">設定中含有錯誤</string>
+ <string name="ipv4_format_error">解析IPv4地址時發生錯誤</string>
+ <string name="custom_route_format_error">解析自訂路由時發生錯誤</string>
+ <string name="vpn_launch_title">連接到VPN</string>
+ <string name="shortcut_profile_notfound">在快捷方式找不到指定的設定檔</string>
+ <string name="route_rejected">路由被Android拒絕</string>
+ <string name="cancel_connection">斷線</string>
+ <string name="clear_log">清除記錄檔</string>
+ <string name="title_cancel">確認取消</string>
+ <string name="edit_vpn">編輯VPN設定</string>
+ <string name="tun_error_helpful">一些自訂的Android4.0 ROM存在/dev/tun的擁有者權限問題,甚至完全沒有Tun模組。CM9用家請嘗試於\"全域設置\"下修正Tun擁有者。</string>
+ <string name="tun_open_error">無法開啟Tun網絡介面</string>
+ <string name="error">"錯誤: "</string>
+ <string name="clear">清除</string>
+ <string name="info">資訊</string>
+ <string name="show_connection_details">顯示連線的詳細資訊</string>
+ <string name="local_ip_info">本地IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS伺服器: %s</string>
+ <string name="dns_domain_info">DNS網域: %s</string>
+ <string name="routes_info">路徑: %s</string>
+ <string name="routes_info6">IPv6路由: %s</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">分享記錄檔</string>
+ <string name="send">分享</string>
+ <string name="bitmask_openvpn_log_file">LEAP Android 記錄檔</string>
+ <string name="copied_entry">已將記錄複製到剪貼簿</string>
+ <string name="no_vpn_support_image">你的Android ROM不支援VPN服務API,抱歉了。 :(</string>
+ <string name="speed_waiting">等待狀態訊息...</string>
+ <string name="error_empty_username">使用者名稱不能為空。</string>
+ <string name="pkcs12_file_encryption_key">PKCS12檔加密金鑰</string>
+ <string name="private_key_password">私密金鑰密碼</string>
+ <string name="password">密碼</string>
+ <string name="building_configration">正在生成設定檔…</string>
+ <string name="netstatus">網絡狀態: %s</string>
+ <string name="mobile_info">於 %1$s (%2$s) %3$s 上運行, Android API 版本: %4$d</string>
+ <string name="translationby">繁體中文 由 羊羊@自由網絡研究中心 &lt;sora8964@gmail.com&gt; 翻譯</string>
+ <string name="warn_no_dns">沒有任何DNS伺服器可用,可能無法進行網域名稱解析。請考慮設置自訂的DNS伺服器</string>
+ <string name="getproxy_error">取得代理伺服器資訊時發生錯誤: %s</string>
+ <string name="using_proxy">使用代理伺服器 %1$s %2$d</string>
+ <string name="ignore">忽略</string>
+ <string name="restart">重置</string>
+ <string name="restart_vpn_after_change">配置變更只會在重新啟動VPN時才生效,現在要(重新)啟動VPN嗎?</string>
+ <string name="configuration_changed">設定已變更</string>
+
+ <string name="minidump_generated">OpenVPN非預期地崩潰,你或者會考慮在主選單下傳送Minidump給開發人員。</string>
+</resources>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
new file mode 100644
index 00000000..66f07a87
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- Keep the order the same as the TYPE_ constants in VPNProfile -->
+</resources>
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
new file mode 100644
index 00000000..cb503b86
--- /dev/null
+++ b/app/src/main/res/values/attrs.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<resources>
+ <declare-styleable name="FileSelectLayout">
+ <attr name="title" format="string|reference" />
+<!-- <attr name="taskid" format="integer" /> -->
+ </declare-styleable>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..bcb6ecc9
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="cancel">Cancel</string>
+ <string name="ok">OK</string>
+ <string name="retry">Retry</string>
+ <string name="repository_url_text">Source code and issue tracker available at https://github.com/leapcode/bitmask_android/</string>
+ <string name="translation_project_text">Translations welcome and appreciated. See our Transifex project at https://www.transifex.com/projects/p/bitmask-android/</string>
+ <string name="copyright_others">This program uses the following components; see the source code for full details on the licenses</string>
+ <string name="about">About Bitmask</string>
+ <string name="switch_provider_menu_option">Switch provider</string>
+ <string name="no_error_found">No error found</string>
+ <string name="config_error_found">Error in Configuration</string>
+ <string name="ipv4_format_error">Error parsing the IPv4 address</string>
+ <string name="custom_route_format_error">Error parsing the custom routes</string>
+ <string name="vpn_launch_title">Connect to VPN</string>
+ <string name="shortcut_profile_notfound">Profile specified in shortcut not found</string>
+ <string name="route_rejected">Route rejected by Android</string>
+ <string name="cancel_connection">Disconnect</string>
+ <string name="clear_log">clear log</string>
+ <string name="title_cancel">Cancel Confirmation</string>
+ <string name="cancel_connection_query">Disconnect the connected VPN/cancel the connection attempt?</string>
+ <string name="edit_vpn">Edit VPN Settings</string>
+ <string name="tun_error_helpful">On some custom ICS images the permission on /dev/tun might be wrong, or the tun module might be missing completely.</string>
+ <string name="tun_open_error">Failed to open the tun interface</string>
+ <string name="error">"Error: "</string>
+ <string name="clear">Clear</string>
+ <string name="info">info</string>
+ <string name="show_connection_details">Show connection details</string>
+ <string name="last_openvpn_tun_config">Last interface configuration from OpenVPN:</string>
+ <string name="local_ip_info">Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d</string>
+ <string name="dns_server_info">DNS Server: %s</string>
+ <string name="dns_domain_info">DNS Domain: %s</string>
+ <string name="routes_info">Routes: %s</string>
+ <string name="routes_info6">Routes IPv6: %s</string>
+ <string name="ip_not_cidr">Got interface information %1$s and %2$s, assuming second address is peer address of remote. Using /32 netmask for local IP. Mode given by OpenVPN is \"%3$s\".</string>
+ <string name="route_not_cidr">Cannot make sense of %1$s and %2$s as IP route with CIDR netmask, using /32 as netmask.</string>
+ <string name="route_not_netip">Corrected route %1$s/%2$s to %3$s/%2$s</string>
+ <string name="keychain_access">Cannot access the Android Keychain Certificates. This can be caused by a firmware upgrade or by restoring a backup of the app/app settings. Please edit the VPN and reselect the certificate under basic settings to recreate the permission to access the certificate.</string>
+ <string name="version_info">%1$s %2$s</string>
+ <string name="send_logfile">Send log file</string>
+ <string name="send">Send</string>
+ <string name="bitmask_openvpn_log_file">Bitmask OpenVPN log file</string>
+ <string name="copied_entry">Copied log entry to clip board</string>
+ <string name="no_vpn_support_image">Your image does not support the VPNService API, sorry :(</string>
+ <string name="opentun_no_ipaddr">Refusing to open tun device without IP information</string>
+ <string name="speed_waiting">Waiting for state message…</string>
+ <string name="converted_profile">imported profile</string>
+ <string name="converted_profile_i">imported profile %d</string>
+ <string name="error_empty_username">The username must not be empty.</string>
+ <string name="pkcs12_file_encryption_key">PKCS12 File Encryption Key</string>
+ <string name="private_key_password">Private Key Password</string>
+ <string name="password">Password</string>
+ <string name="building_configration">Building configuration…</string>
+ <string name="cert_from_keystore">Got certificate \'%s\' from Keystore</string>
+ <string name="netstatus">Network Status: %s</string>
+ <string name="keychain_nocacert">No CA Certificate returned while reading from Android keystore. Authentication will probably fail.</string>
+ <string name="mobile_info">Running on %1$s (%2$s) %3$s, Android API %4$d</string>
+ <string name="error_rsa_sign">Error signing with Android keystore key %1$s: %2$s</string>
+ <string name="translationby">English translation by Arne Schwabe &lt;arne@rfc2549.org&gt;</string>
+ <string name="warn_no_dns">No DNS servers being used. Name resolution may not work. Consider setting custom DNS Servers</string>
+ <string name="dns_add_error">Could not add DNS Server \"%1$s\", rejected by the system: %2$s</string>
+ <string name="getproxy_error">Error getting proxy settings: %s</string>
+ <string name="using_proxy">Using proxy %1$s %2$d</string>
+ <string name="ignore">Ignore</string>
+ <string name="restart">Restart</string>
+ <string name="restart_vpn_after_change">Configuration changes are applied after restarting the VPN. (Re)start the VPN now?</string>
+ <string name="configuration_changed">Configuration changed</string>
+ <string name="minidump_generated">OpenVPN crashed unexpectedly. Please consider using the send Minidump option in the main menu</string>
+ <string name="notifcation_title">Bitmask - %s</string>
+ <string name="state_connecting">Connecting</string>
+ <string name="state_wait">Waiting for server reply</string>
+ <string name="state_auth">Authenticating</string>
+ <string name="state_get_config">Getting client configuration</string>
+ <string name="state_assign_ip">Assigning IP addresses</string>
+ <string name="state_add_routes">Adding routes</string>
+ <string name="state_connected">Connected</string>
+ <string name="state_reconnecting">Reconnecting</string>
+ <string name="state_exiting">Exiting</string>
+ <string name="state_noprocess">Not running</string>
+ <string name="state_resolve">Resolving host names</string>
+ <string name="state_tcp_connect">Connecting (TCP)</string>
+ <string name="state_auth_failed">Authentication failed</string>
+ <string name="state_nonetwork">Waiting for usable network</string>
+ <string name="menu_settings">Settings</string>
+ <string name="title_activity_dashboard">Bitmask</string>
+ <string name="provider_label">Provider:</string>
+ <string name="provider_label_none">No provider configured</string>
+ <string name="eip_settings_button_description">Access EIP connection settings</string>
+ <string name="status_unknown">Status unknown.</string>
+ <string name="future_anonymous_secured_status">Connection will be secure using an anonymous certificate.</string>
+ <string name="anonymous_secured_status">Connection secure using an anonymous certificate.</string>
+ <string name="future_authed_secured_status">Connection will be secure using your own certificate.</string>
+ <string name="authed_secured_status">Connection secure using your own certificate.</string>
+ <string name="eip_service_label">Encrypted Internet</string>
+ <string name="title_activity_configuration_wizard">Select a service provider</string>
+ <string name="new_provider_button">Add new Provider</string>
+ <string name="introduce_new_provider">Add a new service provider</string>
+ <string name="save">Save</string>
+ <string name="new_provider_uri">Domain name</string>
+ <string name="valid_url_entered">It seems your URL is well formed</string>
+ <string name="not_valid_url_entered">It seems your URL is not well formed</string>
+ <string name="provider_details_fragment_title">Provider details</string>
+ <string name="use_anonymously_button">Use anonymously</string>
+ <string name="username_hint">username</string>
+ <string name="username_ask">Please enter your username</string>
+ <string name="password_hint">password</string>
+ <string name="user_message">User message</string>
+ <string name="title_about_activity">About Bitmask"</string>
+ <string name="error_srp_math_error_user_message">Try again: server math error.</string>
+ <string name="error_bad_user_password_user_message">Incorrect username or password.</string>
+ <string name="error_not_valid_password_user_message">It should have at least 8 characters.</string>
+ <string name="error_client_http_user_message">Try again: Client HTTP error</string>
+ <string name="error_io_exception_user_message">Try again: I/O error</string>
+ <string name="error_json_exception_user_message">Try again: Bad response from the server</string>
+ <string name="error_no_such_algorithm_exception_user_message">Update the app</string>
+ <string name="login_button">Log In</string>
+ <string name="logout_button">Log Out</string>
+ <string name="danger_checkbox">Skip security check</string>
+ <string name="setup_error_title">Configuration Error</string>
+ <string name="setup_error_configure_button">Configure</string>
+ <string name="setup_error_close_button">Exit</string>
+ <string name="setup_error_text">There was an error configuring Bitmask with your chosen provider.\n\nYou may choose to reconfigure, or exit and configure a provider upon next launch.</string>
+ <string name="server_unreachable_message">Server is unreachable, please try again.</string>
+ <string name="malformed_url">It doesn\'t seem to be a Bitmask provider.</string>
+ <string name="certificate_error">This is not a trusted Bitmask provider.</string>
+ <string name="configuring_provider">Configuring provider</string>
+ <string name="incorrectly_downloaded_certificate_message">Your anon cert was not downloaded</string>
+ <string name="authenticating_message">Logging in</string>
+ <string name="logout_message">Logging out from this session.</string>
+ <string name="log_out_failed_message">Didn\'t logged out.</string>
+ <string name="succesful_authentication_message">Authentication succeeded.</string>
+ <string name="authentication_failed_message">Authentication failed.</string>
+ <string name="successful_authed_cert_downloaded_message">Your own cert has been correctly downloaded.</string>
+ <string name="authed_cert_download_failed_message">Your own cert has incorrectly been downloaded.</string>
+ <string name="eip_status_start_pending">Initiating connection</string>
+ <string name="eip_cancel_connect_title">Cancel connection?</string>
+ <string name="eip_cancel_connect_text">There is a connection attempt in progress. Do you wish to cancel it?</string>
+ <string name="eip_cancel_connect_cancel">Yes</string>
+ <string name="eip_cancel_connect_false">No</string>
+ <string name="eip_state_not_connected">"Not running! Connection not secure!"</string>
+ <string name="eip_state_connected">Connection Secure.</string>
+</resources>
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..1c20cbcd
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="item">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
+ <item name="android:singleLine">true</item>
+ </style>
+
+ <style name="faqitem">
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ </style>
+
+
+ <style name="faqhead">
+ <item name="android:paddingTop">10sp</item>
+ <item name="android:layout_width">match_parent</item>
+ <item name="android:layout_height">wrap_content</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceMedium</item>
+ <!-- <item name="android:singleLine">true</item> -->
+ </style>
+
+ <style name="accountSetupButton">
+ <item name="android:layout_width">160sp</item>
+ <item name="android:layout_height">40sp</item>
+ <item name="android:textColor">?android:attr/textColorPrimary</item>
+ <item name="android:textAppearance">?android:attr/textAppearanceSmall</item>
+ </style>
+
+ <style name="Theme.CreateShortCut" parent="android:Theme.Holo.DialogWhenLarge">
+ </style>
+
+
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/untranslatable.xml b/app/src/main/res/values/untranslatable.xml
new file mode 100644
index 00000000..6435bfbf
--- /dev/null
+++ b/app/src/main/res/values/untranslatable.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="app" translatable="false">Bitmask</string>
+ <string name="app_name" translatable="false">Bitmask</string>
+
+ <string name="copyright_leapgui" translatable="false">Copyright 2012\nLEAP Encryption Access Project &lt;info@leap.se></string>
+ <string name="opevpn_copyright" translatable="false">Copyright © 2002–2010 OpenVPN Technologies, Inc. &lt;sales@openvpn.net>\n
+
+ "OpenVPN" is a trademark of OpenVPN Technologies, Inc.</string>
+ <string name="lzo_copyright" translatable="false">Copyright © 1996 – 2011 Markus Franz Xaver Johannes Oberhumer</string>
+ <string name="copyright_openssl" translatable="false"> This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit\n
+ Copyright © 1998-2008 The OpenSSL Project. All rights reserved.\n\n
+ This product includes cryptographic software written by Eric Young (eay@cryptsoft.com)\n
+ Copyright © 1995-1998 Eric Young (eay@cryptsoft.com) All rights reserved.</string>
+ <string name="openvpn" translatable="false">OpenVPN</string>
+ <string name="lzo" translatable="false">LZO</string>
+ <string name="openssl" translatable="false">OpenSSL</string>
+ <string name="unknown_state" translatable="false">Unknown state</string>
+</resources>