From 1684c8f398922065a97e7da4dac4ac6a33cc5218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Parm=C3=A9nides=20GV?= Date: Wed, 9 Apr 2014 16:03:55 +0200 Subject: Back to the standard "app" module. This return to "app" instead of "bitmask_android" is due to this reading: https://developer.android.com/sdk/installing/studio-build.html#projectStructure I'll have to tweak the final apk name in build.gradle. --- app/src/main/AndroidManifest.xml | 82 +++ app/src/main/assets/minivpn.armeabi | Bin 0 -> 5216 bytes app/src/main/assets/minivpn.armeabi-v7a | Bin 0 -> 5228 bytes app/src/main/assets/minivpn.mips | Bin 0 -> 5164 bytes app/src/main/assets/minivpn.x86 | Bin 0 -> 5268 bytes app/src/main/assets/urls/bitmask demo.url | 3 + .../java/org/jboss/security/srp/SRPParameters.java | 150 ++++ .../org/spongycastle/util/encoders/Base64.java | 121 ++++ .../spongycastle/util/encoders/Base64Encoder.java | 298 ++++++++ .../org/spongycastle/util/encoders/Encoder.java | 17 + .../util/io/pem/PemGenerationException.java | 26 + .../org/spongycastle/util/io/pem/PemHeader.java | 66 ++ .../org/spongycastle/util/io/pem/PemObject.java | 62 ++ .../util/io/pem/PemObjectGenerator.java | 7 + .../org/spongycastle/util/io/pem/PemWriter.java | 138 ++++ .../java/se/leap/bitmaskclient/AboutActivity.java | 40 ++ .../java/se/leap/bitmaskclient/ConfigHelper.java | 194 ++++++ .../main/java/se/leap/bitmaskclient/Dashboard.java | 479 +++++++++++++ .../leap/bitmaskclient/DownloadFailedDialog.java | 94 +++ app/src/main/java/se/leap/bitmaskclient/EIP.java | 623 +++++++++++++++++ .../se/leap/bitmaskclient/EipServiceFragment.java | 306 +++++++++ .../java/se/leap/bitmaskclient/LeapHttpClient.java | 77 +++ .../java/se/leap/bitmaskclient/LeapSRPSession.java | 341 +++++++++ .../java/se/leap/bitmaskclient/LogInDialog.java | 151 ++++ .../main/java/se/leap/bitmaskclient/PRNGFixes.java | 338 +++++++++ .../main/java/se/leap/bitmaskclient/Provider.java | 212 ++++++ .../bitmaskclient/ProviderAPIResultReceiver.java | 56 ++ .../se/leap/bitmaskclient/ProviderListAdapter.java | 114 ++++ .../leap/bitmaskclient/ProviderListFragment.java | 234 +++++++ app/src/main/java/se/leap/openvpn/CIDRIP.java | 58 ++ .../main/java/se/leap/openvpn/ConfigParser.java | 569 ++++++++++++++++ app/src/main/java/se/leap/openvpn/LICENSE.txt | 24 + app/src/main/java/se/leap/openvpn/LaunchVPN.java | 385 +++++++++++ app/src/main/java/se/leap/openvpn/LogWindow.java | 340 +++++++++ .../java/se/leap/openvpn/NetworkSateReceiver.java | 86 +++ app/src/main/java/se/leap/openvpn/OpenVPN.java | 250 +++++++ .../main/java/se/leap/openvpn/OpenVPNThread.java | 130 ++++ .../se/leap/openvpn/OpenVpnManagementThread.java | 592 ++++++++++++++++ .../main/java/se/leap/openvpn/OpenVpnService.java | 504 ++++++++++++++ .../main/java/se/leap/openvpn/ProfileManager.java | 220 ++++++ .../main/java/se/leap/openvpn/ProxyDetection.java | 54 ++ .../main/java/se/leap/openvpn/VPNLaunchHelper.java | 76 +++ app/src/main/java/se/leap/openvpn/VpnProfile.java | 758 +++++++++++++++++++++ app/src/main/res/drawable-hdpi/ic_menu_add.png | Bin 0 -> 2194 bytes app/src/main/res/drawable-hdpi/ic_menu_login.png | Bin 0 -> 1656 bytes .../drawable-hdpi/ic_menu_settings_holo_light.png | Bin 0 -> 1227 bytes .../res/drawable-hdpi/ic_menu_trash_holo_light.png | Bin 0 -> 1001 bytes .../res/drawable-hdpi/ic_sysbar_quicksettings.png | Bin 0 -> 773 bytes .../main/res/drawable-hdpi/ic_vpn_disconnected.png | Bin 0 -> 801 bytes app/src/main/res/drawable-hdpi/icon.png | Bin 0 -> 7965 bytes app/src/main/res/drawable-ldpi/ic_menu_add.png | Bin 0 -> 1580 bytes app/src/main/res/drawable-ldpi/ic_menu_login.png | Bin 0 -> 1512 bytes app/src/main/res/drawable-ldpi/ic_stat_vpn.png | Bin 0 -> 461 bytes .../main/res/drawable-ldpi/ic_vpn_disconnected.png | Bin 0 -> 455 bytes app/src/main/res/drawable-ldpi/icon.png | Bin 0 -> 3118 bytes app/src/main/res/drawable-mdpi/ic_menu_add.png | Bin 0 -> 1339 bytes .../drawable-mdpi/ic_menu_settings_holo_light.png | Bin 0 -> 866 bytes .../res/drawable-mdpi/ic_menu_trash_holo_light.png | Bin 0 -> 746 bytes .../res/drawable-mdpi/ic_sysbar_quicksettings.png | Bin 0 -> 653 bytes .../main/res/drawable-mdpi/ic_vpn_disconnected.png | Bin 0 -> 586 bytes app/src/main/res/drawable-mdpi/icon.png | Bin 0 -> 4518 bytes app/src/main/res/drawable-xhdpi/ic_menu_login.png | Bin 0 -> 2178 bytes .../drawable-xhdpi/ic_menu_settings_holo_light.png | Bin 0 -> 1622 bytes .../drawable-xhdpi/ic_menu_trash_holo_light.png | Bin 0 -> 1279 bytes .../res/drawable-xhdpi/ic_sysbar_quicksettings.png | Bin 0 -> 956 bytes .../res/drawable-xhdpi/ic_vpn_disconnected.png | Bin 0 -> 1091 bytes app/src/main/res/drawable-xhdpi/icon.png | Bin 0 -> 11135 bytes app/src/main/res/layout-xlarge/about.xml | 134 ++++ .../main/res/layout-xlarge/client_dashboard.xml | 69 ++ .../configuration_wizard_activity.xml | 27 + .../res/layout-xlarge/eip_service_fragment.xml | 75 ++ app/src/main/res/layout-xlarge/log_in_dialog.xml | 39 ++ app/src/main/res/layout-xlarge/logwindow.xml | 17 + .../res/layout-xlarge/provider_detail_fragment.xml | 43 ++ .../res/layout-xlarge/provider_list_fragment.xml | 16 + .../main/res/layout-xlarge/provider_list_item.xml | 44 ++ app/src/main/res/layout/about.xml | 122 ++++ app/src/main/res/layout/client_dashboard.xml | 67 ++ .../res/layout/configuration_wizard_activity.xml | 26 + app/src/main/res/layout/eip_service_fragment.xml | 71 ++ app/src/main/res/layout/log_in_dialog.xml | 41 ++ app/src/main/res/layout/logwindow.xml | 17 + .../main/res/layout/provider_detail_fragment.xml | 40 ++ app/src/main/res/layout/provider_list_fragment.xml | 15 + app/src/main/res/layout/provider_list_item.xml | 43 ++ app/src/main/res/menu/client_dashboard.xml | 23 + .../res/menu/configuration_wizard_activity.xml | 15 + app/src/main/res/menu/logmenu.xml | 34 + app/src/main/res/values-ca/arrays.xml | 3 + app/src/main/res/values-ca/strings.xml | 41 ++ app/src/main/res/values-cs/arrays.xml | 3 + app/src/main/res/values-cs/strings.xml | 79 +++ app/src/main/res/values-de/arrays.xml | 3 + app/src/main/res/values-de/strings.xml | 80 +++ app/src/main/res/values-es/arrays.xml | 3 + app/src/main/res/values-es/strings.xml | 60 ++ app/src/main/res/values-et/arrays.xml | 3 + app/src/main/res/values-et/strings.xml | 66 ++ app/src/main/res/values-fr/arrays.xml | 3 + app/src/main/res/values-fr/strings.xml | 65 ++ app/src/main/res/values-id/arrays.xml | 3 + app/src/main/res/values-id/strings.xml | 80 +++ app/src/main/res/values-it/arrays.xml | 3 + app/src/main/res/values-it/strings.xml | 66 ++ app/src/main/res/values-ja/arrays.xml | 3 + app/src/main/res/values-ja/strings.xml | 82 +++ app/src/main/res/values-ko/arrays.xml | 3 + app/src/main/res/values-ko/strings.xml | 65 ++ app/src/main/res/values-nl/arrays.xml | 3 + app/src/main/res/values-nl/strings.xml | 33 + app/src/main/res/values-no/arrays.xml | 3 + app/src/main/res/values-no/strings.xml | 38 ++ app/src/main/res/values-ro/arrays.xml | 3 + app/src/main/res/values-ro/strings.xml | 51 ++ app/src/main/res/values-ru/arrays.xml | 3 + app/src/main/res/values-ru/strings.xml | 80 +++ app/src/main/res/values-uk/arrays.xml | 3 + app/src/main/res/values-uk/strings.xml | 81 +++ app/src/main/res/values-zh-rCN/arrays.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 52 ++ app/src/main/res/values-zh-rTW/arrays.xml | 3 + app/src/main/res/values-zh-rTW/strings.xml | 52 ++ app/src/main/res/values/arrays.xml | 4 + app/src/main/res/values/attrs.xml | 8 + app/src/main/res/values/strings.xml | 142 ++++ app/src/main/res/values/styles.xml | 37 + app/src/main/res/values/untranslatable.xml | 19 + 127 files changed, 10585 insertions(+) create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/minivpn.armeabi create mode 100644 app/src/main/assets/minivpn.armeabi-v7a create mode 100644 app/src/main/assets/minivpn.mips create mode 100644 app/src/main/assets/minivpn.x86 create mode 100644 app/src/main/assets/urls/bitmask demo.url create mode 100644 app/src/main/java/org/jboss/security/srp/SRPParameters.java create mode 100644 app/src/main/java/org/spongycastle/util/encoders/Base64.java create mode 100644 app/src/main/java/org/spongycastle/util/encoders/Base64Encoder.java create mode 100644 app/src/main/java/org/spongycastle/util/encoders/Encoder.java create mode 100644 app/src/main/java/org/spongycastle/util/io/pem/PemGenerationException.java create mode 100644 app/src/main/java/org/spongycastle/util/io/pem/PemHeader.java create mode 100644 app/src/main/java/org/spongycastle/util/io/pem/PemObject.java create mode 100644 app/src/main/java/org/spongycastle/util/io/pem/PemObjectGenerator.java create mode 100644 app/src/main/java/org/spongycastle/util/io/pem/PemWriter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/AboutActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/Dashboard.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/EIP.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/EipServiceFragment.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/LeapHttpClient.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/LeapSRPSession.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/LogInDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/PRNGFixes.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/Provider.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderListAdapter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderListFragment.java create mode 100644 app/src/main/java/se/leap/openvpn/CIDRIP.java create mode 100644 app/src/main/java/se/leap/openvpn/ConfigParser.java create mode 100644 app/src/main/java/se/leap/openvpn/LICENSE.txt create mode 100644 app/src/main/java/se/leap/openvpn/LaunchVPN.java create mode 100644 app/src/main/java/se/leap/openvpn/LogWindow.java create mode 100644 app/src/main/java/se/leap/openvpn/NetworkSateReceiver.java create mode 100644 app/src/main/java/se/leap/openvpn/OpenVPN.java create mode 100644 app/src/main/java/se/leap/openvpn/OpenVPNThread.java create mode 100644 app/src/main/java/se/leap/openvpn/OpenVpnManagementThread.java create mode 100644 app/src/main/java/se/leap/openvpn/OpenVpnService.java create mode 100644 app/src/main/java/se/leap/openvpn/ProfileManager.java create mode 100644 app/src/main/java/se/leap/openvpn/ProxyDetection.java create mode 100644 app/src/main/java/se/leap/openvpn/VPNLaunchHelper.java create mode 100644 app/src/main/java/se/leap/openvpn/VpnProfile.java create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_add.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_login.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.png create mode 100644 app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.png create mode 100644 app/src/main/res/drawable-hdpi/ic_vpn_disconnected.png create mode 100644 app/src/main/res/drawable-hdpi/icon.png create mode 100644 app/src/main/res/drawable-ldpi/ic_menu_add.png create mode 100644 app/src/main/res/drawable-ldpi/ic_menu_login.png create mode 100644 app/src/main/res/drawable-ldpi/ic_stat_vpn.png create mode 100644 app/src/main/res/drawable-ldpi/ic_vpn_disconnected.png create mode 100644 app/src/main/res/drawable-ldpi/icon.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_add.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.png create mode 100644 app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.png create mode 100644 app/src/main/res/drawable-mdpi/ic_vpn_disconnected.png create mode 100644 app/src/main/res/drawable-mdpi/icon.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_login.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.png create mode 100644 app/src/main/res/drawable-xhdpi/icon.png create mode 100644 app/src/main/res/layout-xlarge/about.xml create mode 100644 app/src/main/res/layout-xlarge/client_dashboard.xml create mode 100644 app/src/main/res/layout-xlarge/configuration_wizard_activity.xml create mode 100644 app/src/main/res/layout-xlarge/eip_service_fragment.xml create mode 100644 app/src/main/res/layout-xlarge/log_in_dialog.xml create mode 100644 app/src/main/res/layout-xlarge/logwindow.xml create mode 100644 app/src/main/res/layout-xlarge/provider_detail_fragment.xml create mode 100644 app/src/main/res/layout-xlarge/provider_list_fragment.xml create mode 100644 app/src/main/res/layout-xlarge/provider_list_item.xml create mode 100644 app/src/main/res/layout/about.xml create mode 100644 app/src/main/res/layout/client_dashboard.xml create mode 100644 app/src/main/res/layout/configuration_wizard_activity.xml create mode 100644 app/src/main/res/layout/eip_service_fragment.xml create mode 100644 app/src/main/res/layout/log_in_dialog.xml create mode 100644 app/src/main/res/layout/logwindow.xml create mode 100644 app/src/main/res/layout/provider_detail_fragment.xml create mode 100644 app/src/main/res/layout/provider_list_fragment.xml create mode 100644 app/src/main/res/layout/provider_list_item.xml create mode 100644 app/src/main/res/menu/client_dashboard.xml create mode 100644 app/src/main/res/menu/configuration_wizard_activity.xml create mode 100644 app/src/main/res/menu/logmenu.xml create mode 100644 app/src/main/res/values-ca/arrays.xml create mode 100644 app/src/main/res/values-ca/strings.xml create mode 100644 app/src/main/res/values-cs/arrays.xml create mode 100644 app/src/main/res/values-cs/strings.xml create mode 100644 app/src/main/res/values-de/arrays.xml create mode 100644 app/src/main/res/values-de/strings.xml create mode 100644 app/src/main/res/values-es/arrays.xml create mode 100644 app/src/main/res/values-es/strings.xml create mode 100644 app/src/main/res/values-et/arrays.xml create mode 100644 app/src/main/res/values-et/strings.xml create mode 100644 app/src/main/res/values-fr/arrays.xml create mode 100644 app/src/main/res/values-fr/strings.xml create mode 100644 app/src/main/res/values-id/arrays.xml create mode 100644 app/src/main/res/values-id/strings.xml create mode 100644 app/src/main/res/values-it/arrays.xml create mode 100644 app/src/main/res/values-it/strings.xml create mode 100644 app/src/main/res/values-ja/arrays.xml create mode 100644 app/src/main/res/values-ja/strings.xml create mode 100644 app/src/main/res/values-ko/arrays.xml create mode 100644 app/src/main/res/values-ko/strings.xml create mode 100644 app/src/main/res/values-nl/arrays.xml create mode 100644 app/src/main/res/values-nl/strings.xml create mode 100644 app/src/main/res/values-no/arrays.xml create mode 100644 app/src/main/res/values-no/strings.xml create mode 100644 app/src/main/res/values-ro/arrays.xml create mode 100644 app/src/main/res/values-ro/strings.xml create mode 100644 app/src/main/res/values-ru/arrays.xml create mode 100644 app/src/main/res/values-ru/strings.xml create mode 100644 app/src/main/res/values-uk/arrays.xml create mode 100644 app/src/main/res/values-uk/strings.xml create mode 100644 app/src/main/res/values-zh-rCN/arrays.xml create mode 100644 app/src/main/res/values-zh-rCN/strings.xml create mode 100644 app/src/main/res/values-zh-rTW/arrays.xml create mode 100644 app/src/main/res/values-zh-rTW/strings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/attrs.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/values/untranslatable.xml (limited to 'app/src/main') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b2366b82 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/minivpn.armeabi b/app/src/main/assets/minivpn.armeabi new file mode 100644 index 00000000..7018dbc9 Binary files /dev/null and b/app/src/main/assets/minivpn.armeabi 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 Binary files /dev/null and b/app/src/main/assets/minivpn.armeabi-v7a differ diff --git a/app/src/main/assets/minivpn.mips b/app/src/main/assets/minivpn.mips new file mode 100644 index 00000000..c44e56c5 Binary files /dev/null and b/app/src/main/assets/minivpn.mips differ diff --git a/app/src/main/assets/minivpn.x86 b/app/src/main/assets/minivpn.x86 new file mode 100644 index 00000000..1a6bf464 Binary files /dev/null and b/app/src/main/assets/minivpn.x86 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 . + */ +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/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 . + */ + 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 + * @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 this 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 . + */ +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 . + */ + 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 + */ +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 + * Activity.RESULT_CANCELED 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> offsets = new TreeMap>(); + JSONObject locationsObjects = null; + Iterator 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 set = offsets.get(dist); + if (set == null) set = new HashSet(); + 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 + */ + private class OVPNGateway { + + private String TAG = "OVPNGateway"; + + private String mName; + private VpnProfile mVpnProfile; + private JSONObject mGateway; + private HashMap>> options = new HashMap>>(); + + + /** + * 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 profiles = vpl.getProfiles(); + for (Iterator 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 arg = new Vector(); + Vector> args = new Vector>(); + + try { + JSONObject def = (JSONObject) eipDefinition.get(common_options); + Iterator keys = def.keys(); + Vector> value = new Vector>(); + while ( keys.hasNext() ){ + String key = keys.next().toString(); + + arg.add(key); + for ( String word : def.getString(key).split(" ") ) + arg.add(word); + value.add( (Vector) arg.clone() ); + options.put(key, (Vector>) 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) arg.clone()); + options.put("remote", (Vector>) 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) arg.clone()); + options.put("location", (Vector>) 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 protocols = new Vector(); + for ( int i=0; i) arg.clone()); + options.put("proto", (Vector>) 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) arg.clone()); + options.put("port", (Vector>) 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 + */ + 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 . + */ + 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 . + */ + 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 . + */ + 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/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 . + */ +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 + * + */ +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. + */ + 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/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 extends ArrayAdapter { + 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 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 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/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 . + */ + 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}. + *

+ * 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 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( + getActivity(), + R.layout.provider_list_item, + ProviderListContent.ITEMS, getArguments().getBoolean(SHOW_ALL_PROVIDERS)); + else + content_adapter = new ProviderListAdapter( + 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 :) +// -- +// bar +// +public class ConfigParser { + + + private HashMap>> options = new HashMap>>(); + 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 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>()); + } + options.get(optionname).add(args); + } + } + public void setDefinition(HashMap>> args) { + options = args; + } + + private void checkinlinefile(Vector args, BufferedReader br) throws IOException, ConfigParseError { + String arg0 = args.get(0); + // CHeck for + if(arg0.startsWith("<") && arg0.endsWith(">")) { + String argname = arg0.substring(1, arg0.length()-1); + String inlinefile = VpnProfile.INLINE_TAG; + + String endtag = String.format("",argname); + do { + String line = br.readLine(); + if(line==null){ + throw new ConfigParseError(String.format("No endtag 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 parseline(String line) throws ConfigParseError { + Vector parameters = new Vector(); + + 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 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> routes = getAllOption("route", 1, 4); + if(routes!=null) { + String routeopt = ""; + for(Vector 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> tlsauthoptions = getAllOption("tls-auth", 1, 2); + if(tlsauthoptions!=null) { + for(Vector 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 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 dev =getOption("dev",1,1); + Vector 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 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 port = getOption("port", 1,1); + if(port!=null){ + np.mServerPort = port.get(1); + } + + Vector proto = getOption("proto", 1,1); + if(proto!=null){ + np.mUseUdp=isUdpProto(proto.get(1));; + } + + // Parse remote config + Vector 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 location = getOption("location",0,2); + if(location != null && location.size() == 2){ + np.mLocation = location.get(1).replace("__", ", "); + } + + Vector> dhcpoptions = getAllOption("dhcp-option", 2, 2); + if(dhcpoptions!=null) { + for(Vector 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 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 cipher = getOption("cipher", 1, 1); + if(cipher!=null) + np.mCipher= cipher.get(1); + + Vector ca = getOption("ca",1,1); + if(ca!=null){ + np.mCaFilename = ca.get(1); + } + + Vector cert = getOption("cert",1,1); + if(cert!=null){ + np.mClientCertFilename = cert.get(1); + np.mAuthenticationType = VpnProfile.TYPE_CERTIFICATES; + noauthtypeset=false; + } + Vector key= getOption("key",1,1); + if(key!=null) + np.mClientKeyFilename=key.get(1); + + Vector pkcs12 = getOption("pkcs12",1,1); + if(pkcs12!=null) { + np.mPKCS12Filename = pkcs12.get(1); + np.mAuthenticationType = VpnProfile.TYPE_KEYSTORE; + noauthtypeset=false; + } + + Vector tlsremote = getOption("tls-remote",1,1); + if(tlsremote!=null){ + np.mRemoteCN = tlsremote.get(1); + np.mCheckRemoteCN=true; + } + + Vector 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 connectretry = getOption("connect-retry", 1, 1); + if(connectretry!=null) + np.mConnectRetry =connectretry.get(1); + + Vector connectretrymax = getOption("connect-retry-max", 1, 1); + if(connectretrymax!=null) + np.mConnectRetryMax =connectretrymax.get(1); + + Vector> 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 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> option:options.values()) { + for(Vector 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 getOption(String option, int minarg, int maxarg) throws ConfigParseError { + Vector> alloptions = getAllOption(option, minarg, maxarg); + if(alloptions==null) + return null; + else + return alloptions.lastElement(); + } + + + private Vector> getAllOption(String option, int minarg, int maxarg) throws ConfigParseError { + Vector> args = options.get(option); + if(args==null) + return null; + + for(Vector 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 vpnlist = mPM.getProfiles(); + + Vector vpnnames=new Vector(); + for (VpnProfile vpnProfile : vpnlist) { + vpnnames.add(vpnProfile.mName); + } + + + + ArrayAdapter adapter = new ArrayAdapter(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: + * + *

    + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_INTENT} The shortcut intent.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_NAME} The text that will be displayed with + * the shortcut.
  • + *
  • {@link android.content.Intent#EXTRA_SHORTCUT_ICON} The shortcut's icon, if provided as a + * bitmap, or {@link android.content.Intent#EXTRA_SHORTCUT_ICON_RESOURCE} if provided as + * a drawable resource.
  • + *
+ * + * 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 myEntries=new Vector(); + + private Handler mHandler; + + private Vector observers=new Vector(); + + + 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 logbuffer; + + private static Vector logListener; + private static Vector 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(); + logListener = new Vector(); + stateListener = new Vector(); + 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 argvlist = new LinkedList(); + + 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 mFDList=new LinkedList(); + 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 active=new Vector(); + + 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 mDnslist=new Vector(); + + private VpnProfile mProfile; + + private String mDomain=null; + + private Vector mRoutes=new Vector(); + private Vector mRoutesv6=new Vector(); + + 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 String joinString(Vector 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 profiles=new HashMap(); + 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 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(); + SharedPreferences listpref = context.getSharedPreferences(PREFS_NAME,Activity.MODE_PRIVATE); + Set vlist = listpref.getStringSet("vpnlist", null); + Exception exp =null; + if(vlist==null){ + vlist = new HashSet(); + } + + 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 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+="\n"+preferences.getString(Provider.CA_CERT, "")+"\n\n"; + cfg+="\n"+preferences.getString(EIP.PRIVATE_KEY, "")+"\n\n"; + cfg+="\n"+preferences.getString(EIP.CERTIFICATE, "")+"\n\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\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 getCustomRoutes() { + Vector cidrRoutes=new Vector(); + 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 getCustomRoutesv6() { + Vector cidrRoutes=new Vector(); + 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 args = new Vector(); + + // 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= 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_add.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_login.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_settings_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu_trash_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_sysbar_quicksettings.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_vpn_disconnected.png 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 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/icon.png 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 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_menu_add.png 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 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_menu_login.png 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 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_stat_vpn.png 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 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_vpn_disconnected.png 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 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/icon.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_add.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_settings_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu_trash_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_sysbar_quicksettings.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_vpn_disconnected.png 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 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/icon.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_login.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_settings_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu_trash_holo_light.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_sysbar_quicksettings.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_vpn_disconnected.png 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 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/icon.png 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + \ 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + + \ 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 @@ + + + + + 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 @@ + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + \ 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 @@ + + + + + + + \ 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 @@ + + + + + + + + + + \ 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 @@ + + + + + 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 @@ + + + + + + + + + + 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 @@ + + + + + + + + + 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 @@ + + + + + + \ 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 @@ + + + + + + + + + + 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 @@ + + + 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 @@ + + + Cancel·la + Quan a + No s\'ha trobat cap error + S\'ha trobat un error a la configuració + Conecta a la VPN + No s\'ha trobat el perfil especificat a l\'accès directe + La ruta ha estat refusas per Android + Desconecta + Neteja el registre + Cancela la confirmació + Desconcta la conexió VPN/cancela l\'intent de conexió? + Edita la configuració VPN + "Error: " + Neteja + info + Mostra els detalls de conexió + Servidor DNS: %s + Domini DNS: %s + Rutes: %s + Rutes IPv6: %s + Envia el fitxer de registre + Envia + Fitxer de registre de LEAP Android + S\'ha copiat l\'entrada al porta-retalls + Esperant el missatge d\'estat... + Perfil importat + Perfil importat %d + Contrasenya de la clau privada + Contrasenya + Construint la configuració... + Estat de la xarxa %s + Traducció al catala per Sergi Almacellas +<sergi@koolpi.com> + Utilitzant el proxy %1$s %2$d + Ignorar + Reinicia + Els canvis de configuració s\'apliquen desprès de reinicar la VPN. (Re)inicar la VPN ara? + S\'ha canviat la configuració + 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 @@ + + + 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 @@ + + + Storno + Zdrojové kódy a seznam problémů je na https://github.com/leapcode/bitmask_android/ + Tento program používá následující komponenty; viz zdrojový kód pro detaily o licenci + O programu + Bez chyb + Chyba v konfiguraci + Chyba při zpracování IPv4 adresy + Chyba při zpracování vlastního směrování + Profil zvolený ve zkratce nenalezen + Směrování odmítnuto Androidem + Odpojit + vymazat log + Zrušit potvrzení + Odpojit/Zrušit připojování? + Změnit nastavení VPN + 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. + Chyba při otvírání tun zařízení + "Chyba: " + Vymazat + info + Zobrazit detaily spojení + Poslední nastavení rozhraní pro OpenVPN: + Místní IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS server: %s + DNS doména: %s + Směrování: %s + Směrování IPv6: %s + 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\". + %1$s a %2$s jako IP adresy s CIDR maskou nedávají smysl, používám /32 jako masku. + Směrování opraveno z %1$s/%2$s na %3$s/%2$s + 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í. + %1$s %2$s + Odeslat soubor s logem + Odeslat + LEAP Android logovací soubor + Záznam z logu zkopírován do schránky + Tvůj obrázek není podporovaný rozhraním VPNService, je mi líto :-( + Odmítám otevřít tun zařízení bez informace o IP + Čekám na zprávu o stavu… + importovaný profil + importovaný profil %d + Jméno nesmí být prázdné. + Šifrovací klíč PKCS12 + Heslo k soukromému klíči + Heslo + Vytvářím konfiguraci… + Získán certifikát \'%s\' z úložiště + Stav sítě: %s + Žádný CA certifikát nebyl získán z úložiště, autentikace pravděpodobně selže. + Spuštěno na %1$s (%2$s) %3$s, Android API %4$d + Chyba při podepisování klíčem %1$s: %2$s + Českou lokalizaci zpracoval Jan Baier <baier.jan@gmail.com> + Není použit DNS server. Překlad jmen nemusí fungovat. Zvaž nastavení vlastního DNS serveru. + Nemohu přidat DNS server \"%1$s\", odmítnuto systémem: %2$s + Chyba při zjišťování nastavení proxy: %s + Používám proxy %1$s %2$d + Ignorovat + Restartovat + Změna nastavení začne platit až po restartu VPN. Restartovat teď? + Nastavení změněno + OpenVPN neočekávaně havarovalo. Zvaž možnost použití volby poslat Minidump z hlavního menu + Bitmask - %s + Připojuji se + Čekání na odpověď serveru + Ověřuji autorizaci + Stahuji konfiguraci klienta + Nastavuji IP adresu + Přidávám trasy + Připojeno + Obnovuji připojení + Ukončuji + Neběží + Překlad názvů + Připojuji (TCP) + Přihlášení nebylo úspěšné + Čekání na použitelnou síť + 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 @@ + + + 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 @@ + + + Abbrechen + Quellcode und Issue Tracker sind verfügbar unter https://github.com/leapcode/bitmask_android/ + Dieses Programm nutzt die folgenden Komponenten. Die kompletten Lizenzdetails sind im Quelltext verfügbar. + Über + Kein Fehler. + Fehler in der Konfiguration + Kann die die IPv4 Adresse nicht parsen + Kann die manuell angegeben Routen nicht parsen + Von der Verknüpfung referenziertes Profil konnte nicht gefunden werden + Route von Android zurückgewiesen. + Trennen + Log löschen. + Trennungsbestätigung + Möchten Sie das VPN trennen bzw. den Verbindungsversuch abbrechen? + Ändere VPN Einstellungen + 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. + Das Öffnen des tun Interfaces ist katastrophal gescheitert + "Fehler: " + Clear + Info + Zeige Verbindungsdetails + Letzte Interface Konfiguration von OpenVPN: + Lokale IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS Server: %s + DNS Domäne: %s + Routen IPv4: %s + Routen IPv6: %s + 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\". + Die Route %1$s mit der Netzmaske %2$s ist keine Route mit einer CIDR Netzmaske, benutze /32 als Netzmaske. + Route %1$s/%2$s korrigiert zu %3$s/%2$s + 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. + %1$s %2$s + Sende Logdatei + Sende + LEAP Android log Datei + Log Eintrag in die Zwischenablage kopiert + Dieses Android ROM enthält keine VPNService API. Sorry :( + Verweigere tun Gerät zu öffnen ohne IP Information + Warte auf OpenVPN Status Nachricht… + Importiertes Profil + Importiertes Profil %d + Der Benutzername darf nicht leer sein + PKCS12 Veschlüsslungspassword + Passphrase privater Schlüssel + Passwort + Generiere OpenVPN Konfiguration… + Zertifikat (KeyStore): \'%s\' + Netzwerkstatus: %s + 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. + Modell %1$s (%2$s) %3$s, Android API %4$d + Fehler beim Zugriff auf den Android Keystore %1$s: %2$s + Deutsche Übersetzung von Arne Schwabe <arne@rfc2549.org> + 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. + Konnte den DNS Server \"%1$s\" nicht hinzufügen, da das System ihn zurückweist mit %2$s + Fehler beim Ermitteln der Proxy Einstellungen: %s + Benutzt Proxy %1$s %2$d + Ignorieren + Neu verbinden + Konfigurationsänderungen werden erst nach einem VPN Neustart aktiv. Jetzt neu verbinden? + Konfiguration geändert + Der OpenVPN Prozess ist unerwartet abgestürzt. Bitte erwägen Sie die Option \"Minidump senden\" im Hauptmenü + Bitmask - %s + + Verbinde + Warte auf Serverantwort + Authentifiziere + Warte auf Clientkonfiguration + Weise IP Adressen zu + Hinzufügen von Routen + Verbunden + Wiederverbinden + Beende + Kein OpenVPN Prozess + Löse Hostnamen auf + Verbinde (TCP) + Authentifizierung fehlgeschlagen + Warte auf Internetverbindung + 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 @@ + + + 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 @@ + + + Cancelar + Codigo fuente y sistema de reporte de errores disponibles en https://github.com/leapcode/bitmask_android/ + El programa utiliza los siguientes componentes. Vea los códigos fuentes para obtener más información sobre las licencias + Acerca de + Ruta rechazada por Android + Desconectar + Limpiar registro + Cancelar confirmación + ¿Desconectar la conexión VPN/cancelar el intento de conexión? + Modificar la configuración de VPN + 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. + La apertura de la interfaz tun falĺó + "Error: " + Borrar + información + Mostrar detalles de la conexión + Última configuración de interfaz de OpenVPN: + Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + Servidor DNS: %s + Dominio DNS: %s + Rutas: %s + Rutas IPv6: %s + 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\". + No tienen sentido %1$s y %2$s como ruta IP con máscara de red CIDR, usando /32 como máscara de red. + Ruta conectada de %1$s/%2$s a %3$s/%2$s + 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. + %1$s %2$s + Enviar el archivo de registro + Enviar + Archivo de registro de LEAP de Android + Entrada de registro copiada al Portapapeles + Su imagen no es compatible con la API de VPNService, lo siento :( + Negandose a abrir el dispositivo tun sin información de IP + Esperando el mensaje de estado... + perfil importado + perfil importado %d + El nombre de usuario no debe estar vacío. + Clave PKCS12 de cifrado de archivos + Contraseña de clave privada + Contraseña + Construyendo configuracion... + Conseguido el certificado de \'%s\' de almacén de claves + Estado de la red: %s + No se obtuvo ningún certificado de CA al leer el almacén de claves de Android. La autenticación probablemente fallará. + Ejecutándose en %1$s (%2$s) %3$s, API de Android %4$d + Error al firmar con la llave del almacén de llaves de Android %1$s: %2$s + Traducción al español por José Luis Bandala Perez<luis.449bp@gmail.com> + Sin servidores DNS utilizados. La resolución de nombres puede que no funcione. Considere configurar servidores DNS personalizados + No se puede agregar el servidor DNS \"%1$s\", rechazado por el sistema: %2$s + Error al obtener la configuración de proxy: %s + Usando proxy %1$s %2$d + Ignorar + Reiniciar + Los cambios de configuración se aplican después de reiniciar la VPN. ¿(Re)iniciar la VPN ahora? + Configuración cambiada + + OpenVPN falló inesperadamente. Por favor considere usar la opción envío de minivolcado en el menú principal + 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 @@ + + + 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 @@ + + + Tühista + Lähtetekst ja probleemihaldur asuvad veebilehel https://github.com/leapcode/bitmask_android/ + Programmis kasutatakse järgnevaid komponente. Detailse litsenseerimisinfo leiate lähtekoodist + Lähemalt + Vigu ei leitud + Konfiguratsiooni viga + Sisestatud IPv4 aadress ei allu süntaksianalüüsile + Kohandatud marsruudid ei allu süntaksianalüüsile + Ühendu VPN\'iga + Lühivalikus määratud profiil puudub + Androidi poolt keelatud ruutingud + Katkesta ühendus + Tühjenda logi + Loobu kinnitusest + Katkesta VPN ühendus/tühista ühendumise katse? + Muuda VPN seadistusi + Mõnel modifitseeritud ICS versioonil võivad /dev/tun õigused olla valed, või selle moodul sootuks puududa. + Tun liidese avamine ebaõnnestus + "Viga:" + Tühjenda + info + Näita ühenduse andmeid + Viimane liidese konfigureerimine OpenVPN poolt: + Lokaalne IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS Server: %s + DNS domeen: %s + Marsruudid: %s + IPv6 marsruudid: %s + 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\". + %1$s ja %2$s on mõttetud CIDR võrgumaskiga IP marsruutidest, võrgumaskiks määratakse /32. + %1$s/%2$s marsruut parandatud: %3$s/%2$s + 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. + %1$s %2$s + Saada logifail + Saada + LEAP Androidile logifail + Logikirje kopeeriti lõikepuhvrisse + Teie süsteemitarkvara ei toeta VPNService API\'t, vabandame :( + IP andmeteta keeldutakse tun liidese avamisest + Ootan olekuteadet… + imporditud profiil + imporditud profiil %d + Kasutajanimi peab olema määratud. + PKCS12 faili krüpteerimisvõti + Privaatse võtme salasõna + Salasõna + Koostatakse konfiguratsiooni… + Saadud sertifikaat \'%s\' võtmehoidlast + Võrgu olek: %s + Androidi võtmehoidlast lugemine ei andnud ühtegi CA sertifikaati. Suure tõenäosusega autentimine ebaõnnestub. + Töötamas %1$s (%2$s) %3$s peal, Android API %4$d + Viga allkirjastamisel Androidi võtmehoidla võtmega %1$s: %2$s + Eesti keelde tõlkis Robert Tiismus + Ühtegi DNS serverit ei kasutata. Nimelahendus ei pruugi töötada. Vae kohandatud DNS serveri kasutust + DNS serveri \"%1$s\" lisamine ebaõnnestus, süsteemi poolt keelduti: %2$s + Viga proxy seadistuste vastuvõtul: %s + Kasutusel proxy %1$s %2$d + Ignoreeri + Uuestilaadimine + Konfiguratsioonimuudatused rakendatakse peale VPN uuestilaadimist. Kas soovite VPN kohe (uuesti)laadida? + Konfiguratsiooni muudeti + + OpenVPN jooksis ootamatult kokku. Palun kaaluge \"saada Minitõmmis\" valiku lubamist peamenüüs + 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 @@ + + + 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 @@ + + + "Annuler" + "Le code source et le tracker de bugs est disponible ici: https://github.com/leapcode/bitmask_android/ " + "Le programme utilise les composants suivants. Voir le code source pour plus de détails sur les licences." + "À propos" + "Aucune erreur" + "Erreur dans la configuration" + "Impossible d\'analyser l\'adresse IPv4" + "Impossible d\'analyser les règles de redirection personnalisés" + "Se connecter au VPN" + "Profil spécifié dans raccourci introuvable" + "Route rejetée par Android" + "Déconnecter" + "Effacer les logs" + "Annuler la confirmation" + "Déconnecter le VPN connecté / annuler la tentative de connexion ?" + "Modifier les paramètres VPN" + "Sur certaines ROMs ICS les permissions de /dev/tun peuvent être incorrectes, ou le module Tun peut être manquant." + "L\'ouverture de l\'interface Tun a échoué." + "Erreur: " + "Effacer" + "infos" + "Afficher les détails de la connexion" + "Dernière configuration connue de l\'interface d\'OpenVPN:" + "IPv4 locale: %1$s/%2$d IPv6: %3$s MTU: %4$d" + "Serveur DNS: %s" + "Domaine DNS: %s" + "Routes: %s" + "Routes IPv6: %s" + "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\"." + "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." + "Règle de redirection corrigée: %1$s / %2$s en %3$s / %2$s" + \"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.\". + "%1$s %2$s" + "Envoyer le fichier de log" + "Envoyer" + "Fichier de log LEAP Android" + "Entrée du log copiée" + "Votre ROM ne prend pas en charge l\'API VPNService, désolé :(" + "Impossible d\'ouvrir le périphérique TUN sans informations IP" + "Attente du status..." + "profil importé" + "profil importé: %d" + "Le nom d\'utilisateur ne doit pas être vide." + "Fichier de clé de cryptage PKCS12" + "Mot de passe de clé privée" + "Mot de passe" + "Création de la configuration ..." + "Certificat \'%s\' obtenu" + "État du réseau: %s" + "Aucun certificat CA renvoyée lors de la lecture depuis le gestionnaire de clés. L\'authentification échouera probablement." + "Fonctionnant sur %1$s (%2$s) %3$s , Android API %4$d" + "Erreur de signature de la clé %1$s : %2$s par le gestionnaire d\'Android" + French translation by Stanislas Bach<stanislasbach@gmail.com> + "Pas de serveurs DNS utilisés. La résolution de noms peut ne pas fonctionner. Envisagez d\'utiliser des serveurs DNS personnalisés." + "Impossible d\'ajouter le serveur DNS \"%1$s\", rejetés par le système: %2$s" + "Erreur d\'obtention des paramètres de proxy: %s" + "Utilisation du proxy %1$s %2$d" + "Ignorer" + "Redémarrer" + "Les changements de configuration sont appliquées après redémarrage du VPN. (Re)démarrer le VPN maintenant?" + "Configuration modifiée" + + 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 @@ + + + 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 @@ + + + Batal + Kode program dan perekam masalah tersedia di + Aplikasi memakai komponen berikut; lihat kode program untuk lebih jelas mengenai lisensi + Tentang... + Tidak ada kesalahan + Gagal menganalisa rute buatan + Hubungkan VPN + Profil di shrotcut tidak ada + Rute ditolak Android + Putus + Bersihkan catatan + Batal Konfirmasi + Putuskan sambungan VPN/Batalkan usaha menyambungkan VPN? + Ubah seting OpenVPN + Pada beberapa setelan manual gambar ICS izin pada/dev/tun mungkin salah, atau modul tun mungkin hilang sepenuhnya. + Gagal membuka layanan antarmuka TUN + "Kesalahan: " + Bersihkan + Info + Detail koneksi + Konfigurasi terakhir dari OpenVPN: + IPv4 lokal : %1$s/%2$d IPv6: %3$s MTU: %4$d + Server DNS : %s + Domain DNS : %s + Rute: %s + Rute IPv6: %s + 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$\". + Tidak masuk akal membuat %1$ s dan %2$ s sebagai rute IP dengan netmask CIDR, Gunakan /32 sebagai netmask. + rute yang diperbaiki %1$s/%2$s hingga %3$s/%2$s + 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. + %1$s %2$s + Kirim berkas catatan + Kirim + Berkas catatan LEAP Android + Salin catatan masuk ke clipboard + Gambar Anda tidak mendukung VPNService API, maaf:( + TUN tidak dapat dibuka tanpa informasi IP + Menunggu pesan status… + Profil yang diambil + profil yang diambil %d + Nama pengguna tidak boleh kosong. + Berkas kunci enkripsi PKCS12 + Sandi kunci pribadi + Sandi + Membuat konfigurasi… + Sertifikat didapatkan \'%s\' dari Keystore + Status jaringan: %s + Tidak ada sertifikat CA yang didapat saat membaca dari Android Keystore. Otentifikasi mungkin gagal + Berjalan di %1$s (%2$s) %3$s, Android API %4$d + Kesalahan masuk dengan kunci Android keystore %1$ s: %2$ s + Terjemah Bahasa Indonesia oleh Dayro + Tidak ada server DNS yang digunakan. Resolusi nama mungkin tidak bisa bekerja. Pertimbangkan seting server DNS yang diatur sendiri + Tak bisa menambahkan Server DNS \"%1$ s\", ditolak oleh sistem: %2$ s + Gagal mendapatkan pengaturan proxy: %s + Menggunakan proxy %1$ s %2$ d + Abaikan + Restart + Perubahan konfigurasi baru diterapkan setelah restart VPN. Restart VPN sekarang? + Konfigurasi berubah + + OpenVPN crash tak terduga. Silakan mempertimbangkan mengirim menggunakan pilihan Minidump di Menu Utama + Bitmask - %s + + Menghubungkan + Menunggu jawaban server + Melakukan otentifikasi + Mengambil konfigurasi klien + Menetapkan alamat IP + Menambahkan rute + Terhubung + Menghubungkan kembali + Keluar + Tidak berjalan + Mengenali nama host + Menghubungkan (TCP) + Otentifikasi gagal + Menunggu jaringan yang dapat dipakai + 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 @@ + + + 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 @@ + + + Annulla + Il codice sorgente e il bug tracker sono disponibili all\'indirizzo https://github.com/leapcode/bitmask_android/ + Questo programma usa i seguenti componenti; guarda il codice sorgente per i dettagli completi sulle licenze + Informazioni + Nessun errore trovato + Errore nella configurazione + Impossibile analizzare l\'indirizzo IPv4 + Errore durante la lettura delle regole di reindirizzamento (routing) + Connetti alla VPN + Profilo indicato nel collegamento non trovato + Reindirizzamento (route) rifiutato da Android + Scollega + Cancella registro + Conferma l\'annullamento + Disconnetti la VPN in uso/annulli il tentativo di connessione? + Modifica impostazioni VPN + In alcune immagini ICS personalizzate i permessi su /dev/tun potrebbero essere errati, oppure il modulo TUN completamente assente. + Impossibile accedere all\'interfaccia tun + "Errore:" + Azzera + Info + Visualizza i dettagli della connessione + Ultima configurazione interfaccia OpenVPN: + Indirizzi locali - IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + Server DNS: %s + Dominio DNS: %s + Instradamenti (route): %s + Instradamenti (route) IPv6: %s + 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\". + Impossibile utilizzare %1$s e %2$s come reindirizzamenti IP con la maschera CIDR, è stata quindi usata la maschera /32. + Instradamento %1$s/%2$s corretto con %3$s/%2$s + 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. + %1$s %2$s + Invia il file di log + Invia + File log di LEAP Android + Voce di registro copiata negli appunti + La tua immagine non è supportata dal VPNService API, mi dispiace :( + Rifiuto di attivare il dispositivo tun senza informazioni sull\'IP + In attesa del messaggio di stato... + profilo importato + profilo importato %d + L\'username non deve essere vuoto. + File con la chiave di crittografia PKCS12 + Password della chiave privata + Password + Configurazione in corso... + Ottenuto il certificato \'%s\' dal Keystore + Stato della rete: %s + Nessun certificato della CA è stato prelevato dal Keystore di Android. E\' probabile che l\'autenticazione fallisca. + In esecuzione su %1$s (%2$s) %3$s, Android API %4$d + Errore di firma con la chiave %1$s: %2$s del Keystore di Android. + Traduzione in inglese di Arne Schwabe<arne@rfc2549.org> + Nessun server DNS in uso. La risoluzione dei nomi potrebbe non funzionare. Valuta se inserire dei server DNS personalizzati. + Impossibile aggiungere il server DNS \"%1$s\", respinto dal sistema: %2$s + Errore nell\'ottenere le impostazioni del proxy: %s + Si sta utilizzando il proxy %1$s %2$d + Ignora + Riavvia + Le modifiche sarannoi applicate dopo aver riavviato la connessione VPN. Riavviare ora la connessione? + Configurazione modificata + + OpenVPN si è arrestato in modo imprevisto. Ti consigliamo di attivare l\'opzione Invia Minidump nel menu principale. + 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 @@ + + + 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 @@ + + + キャンセル + ソースコードと問題管理は以下で: https://github.com/leapcode/bitmask_android/ + プログラムは、次のコンポーネントを使用します。完全な詳細についてはソース上のライセンスを参照してください。 + バージョン情報 + エラーは見つかりませんでした。 + 設定に誤りがあります。 + IPv4 アドレスの解析エラー + カスタム経路の解析エラー + VPNに接続 + ショートカットで指定されたプロファイルが見つかりません + 経路がAndroidにより拒否されました。 + 切断 + ログをクリア + キャンセルの確認 + 接続中または試行中の接続をキャンセルしますか? + VPN 設定の編集 + いくつかのカスタムICSイメージは、/dev/tunのパーミッションが異常か、TUNモジュールが含まれていません。 + TUNデバイスを開こうとして失敗しました。 + "エラー:" + クリア + 情報 + 接続の詳細を表示 + OpenVPNから設定した最後のインターフェイス: + ローカル IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS サーバー: %s + DNS ドメイン: %s + 経路:%s + 経路 IPv6:%s + インターフェース情報として[%1$s]と[%2$s]を取得しました。2つめのアドレスはリモート側のピアアドレスです。32ビットマスクをローカルIPに使用します。 OpenVPNのモードは[%3$s]です。 + %1$sと%2$sではCIDR形式のIP経路情報として意味をなしません。32ビットマスクを使用します。 + 経路情報%1$s/%2$sを%3$s/%2$sに修正しました。 + Androidの証明書管理にアクセスできません。(ファームウェアの更新、アプリケーションまたはその設定のリストアによって発生する場合があります)。VPNの設定で証明書の選択を再度行ってください。 + %1$s %2$s + ログ ファイルを送信します。 + 送信 + LEAP Android ログ ファイル + クリップ ボードにコピーされたログ エントリ + 申し訳ありませんが、お使いの環境ではVPNサービスがサポートされていません。 + IP情報なしでのTUNデバイス使用は拒否しています + 状態メッセージを待っています。 + インポートされたプロファイル + インポートされたプロファイル %d + ユーザ名を空に設定することはできません + PKCS12ファイルの暗号化キー + 秘密鍵のパスワード + パスワード + 構成中・・・ + \'%s\'の証明書をキーストアから読み出し + ネットワーク状態: %s + 認証局証明書(CA Cert)がAndroidのキーストアから取得できませんでした。認証はおそらく失敗します。 + 実行中:%1$s (%2$s)%3$s Android API %4$d + Androidキーストアに保存されたキー %1$s: %2$sの署名エラーです + 日本語翻訳 高橋正希@埼玉 <tools@artin.nu> + 使用されている DNS サーバーはありません。名前解決は動作しません。DNSサーバーの設定を見直してください。 + DNSサーバ \"%1$s\" の追加に失敗しました。%2$sに拒否されました。 + プロキシ設定でエラー: %s + プロキシを使用します %1$s %2$d + 無視 + 再起動 + 設定の変更はVPNの再起動後に反映されます。VPNを(再)起動しますか? + 設定が変更されました + + OpenVPN は予期せず終了しました。メイン メニューでミニダンプの送信オプションを検討してください。 + Bitmask - %s + + 接続中 + サーバーの応答を待っています。 + 認証中 + クライアントの構成を取得中 + IPアドレスを割り当て中 + 経路を追加中 + 接続しました + 再接続中 + 終了中 + 停止中 + ホスト名を解決中 + 接続中(TCP) + 認証に失敗しました + 使用可能なネットワークを待機中 + 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 @@ + + + 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 @@ + + + 소스 코드와 문제 추적기는 https://github.com/leapcode/bitmask_android/에서 사용할 수 있습니다 + 프로그램은 다음 구성 요소를 사용합니다. 라이선스에 대 한 자세한 내용은 소스를 참조 하십시오 + 소개 + 오류 없음 + 설정 오류 + IPv4 주소 구문 분석 오류 + 사용자 지정 경로 구문 분석 오류 + VPN에 연결 하기 + 바로 가기에 지정 된 프로파일을 찾을 수 없습니다. + 안드로이드에 의해 거부된 라우트 + 연결 끊기 + 로그 지우기 + 취소 확인 + 연결된 VPN 끊기/연결시도 취소? + VPN 설정 편집 + 일부 사용자 지정 ICS 이미지에서는 /dev/tun에 대한 권한이 잘못 되어 있거나 tun 모듈 자체가 누락 될 수 있습니다. + Tun 인터페이스를 열지 못했습니다 + "오류:" + 지우기 + 정보 + 연결 세부 정보 보기 + Openvpn에서 마지막 인터페이스 구성: + 로컬 IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS 서버: %s + DNS 도매인: %s + 라우트: %s + 라우트 IPv6: %s + 인터페이스 정보 %1$s와 %2$s에 있어, 두 번째 주소를 원격 피어 주소로 가정 하겠습니다. 로컬 IP의 넷마스크로는 /32를 사용하겠습니다. OpenVPN에 의해 주어진 모드는 \"%3$s\" 입니다. + CIDR 넷마스크가 있는 IP 라우트 %1$s 와 %2$s 에 있어서 처리가 불가능합니다. /32를 넷마스크로 사용하겠습니다. + %1$s/%2$s 에서 %3$s/%2$s로 라우트 수정 + 안드로이드 키체인 인증서에 접근할 수 없습니다. 펌웨어 업그래이드 또는 백업된 앱/앱 설정을 복구하면서 발생할 수 있습니다. 인증서에 액세스할 수 있는 권한을 다시 생성하기 위해 VPN을 편집 하고 기본 설정 아래에서 인증서를 다시 선택 하십시오. + %1$s %2$s + 로그 파일 보내기 + 보내기 + LEAP 로그 파일 + 클립보드로 로그 복사 + 당신의 이미지는 VPNService API를 지원 하지 않습니다, 죄송 합니다:( + IP 정보가 없는 tun 장치 열기를 거부합니다 + 상태 메시지를 기다리는 중... + 가져온 프로파일 + 가져온 프로파일 %d + 사용자 이름이 비어 있지 않아야 합니다. + PKCS12 파일 암호화 키 + 개인 키 암호 + 암호 + 설정 만드는중… + Keystore에서 인증서 \'%s\' 받음 + 네트워크 상태: %s + 안드로이드 keystore에서 CA 인증서를 찾지 못했습니다. Auhtentication은 실패할 것 입니다. + %1$s (%2$s) %3$s, 안드로이드 API %4$d 에서 실행 + 안드로이드 keystore 키 %1$s: %2$s과 싸이닝 오류 + 한국어 번역 (주)기가드 안규태<ktdann@gmail.com> + 사용 중인 DNS 서버가 없습니다. 이름 확인이 작동 하지 않을 수 있습니다. 사용자 지정 DNS 서버의 사용을 고려해 보세요. + DNS서버 \"%1$s\" 는, 시스템에 의해 거부되 추가할 수 없습니다: %2$s + 프록시 설정 가져오기 오류: %s + 프록시 %1$s %2$d 을 사용 + 무시 + 다시 시작 + VPN을 다시 시작한 후 설정 변경 내용이 적용 됩니다. VPN을 지금 (재)시작? + 설정 변경 + + OpenVPN이 예기치 않게 종료됐습니다. 메인 메뉴에 있는 미니 덤프 보내기 옵션의 사용을 고려 하시기 바랍니다 + 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 @@ + + + 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 @@ + + + Over + Geen fout. + Fout in de configuratie + Met VPN verbinden + Route geweigert door Android + Verbinding verbreken + logboek wissen + Annuleer bevestiging + Sluit de verbonden VPN af/annuleer de verbindingspoging? + VPN Instellingen Bewerken + "Fout:" + Leeg maken + info + Details van de verbinding weergeven + Laatste interfaceconfiguratie van OpenVPN: + Lokaal IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS Server: %s + DNS Domein: %s + Routes: %s + Routes IPv6: %s + %1$s %2$s + Logboek verzenden + Verzenden + Wachten op status bericht… + Geïmporteerd profiel + Geïmporteerd profiel %d + De gebruikersnaam moet niet leeg zijn. + PKCS12 Bestand Encryptie Sleutel + Privé Sleutel Wachtwoord + Wachtwoord + 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 @@ + + + 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 @@ + + + Om + Ingen feil funnet + Feil i konfigurasjonen + Koble til VPN + Koble fra + Tøm logg + Avbryt bekreftelse + Rediger VPN-innstillinger + "Feil:" + Fjern + info + Vis Tilkoblingsdetaljer + DNS-server: %s + DNS-domene: %s + Ruter: %s + %1$s %2$s + Send loggfilen + Send + LEAP Android loggfil + Venter på tilstands melding... + importert profil + importert profilen %d + Brukernavnet kan ikke være tomt. + PKCS12 Filkrypteringsnøkkel + Privat nøkkel passord + Passord + Lager konfigurasjon... + Nettverksstatus: %s + Kjører på %1$s (%2$s) %3$s, Android API %4$d + Norsk oversettelse av Jonny + Feil ved henting av proxy-innstillinger: %s + Bruker proxy %1$s %2$d + Ignorer + Start på nytt + Konfigurasjon endret + 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 @@ + + + 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 @@ + + + Cod sursă şi tracker probleme disponibile la https://github.com/leapcode/bitmask_android/ + Acest program utilizează următoarele componente; a se vedea codul sursă pentru mai multe detalii despre licente + Despre + Nu s-a găsit nici o eroare + Eroare în configurare + Eroare parsare adresă IPv4 + Eroare parsare rute particularizate + Conectare la VPN + Profilul specificat în comanda rapidă nu a fost găsit + Rută respinsă de Android + Deconectaţi + Golire jurnal + Anulare confirmare + Deconectaţi VPN-ul conectat/anulaţi încercarea de conectare? + Editaţi setările VPN + Î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. + Eroare deschidere interfaţa tun + "Eroare:" + Goleşte + info + Arată detaliile conexiunii + Ultima configurare interfaţă din OpenVPN: + Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + Server DNS: %s + Domeniu DNS: %s + Rute: %s + Rute IPv6: %s + 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\". + %1$s %2$s + Trimite fişier jurnal + Trimite + Fişier jurnal LEAP Android + profil importat + profil importat %d + Cheie criptare fişier PKCS12 + Parola cheie privată + Parola + Se generează configurarea… + Am primit certificatul \'%s\' din Keystore + Statutus reţea: %s + Rulează pe %1$s (%2$s) %3$s, Android API %4$d + Eroare semnare cu Android keystore key %1$s: %2$s + Folosesc proxy %1$ s %2$ d + Ignora + Restart + Configuraţie schimbată + + OpenVPN sa oprit în mod neaşteptat. Vă rugăm să consideraţi opţiunea de trimitere a unui Minidump din meniul principal + 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 @@ + + + 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 @@ + + + Данная программа использует следующие компоненты; смотрите исходный код для получения подробной информации о лицензии + О программе + Ошибок не найдено + Ошибка в конфигурации + Невозможно прочесть IPv4 адрес + Невозможно примениить пользовательские маршруты + Подключиться к VPN + Не найден профиль, указанный в ярлыке + Маршрут отвергнут Android + Отключение + очистить журнал + Подтверждение отмены + Отключение активных VPN/Отмена попыток подключения? + Редактирование параметров VPN + На некторых костомных сборках права на /dev/tun могут быть неверными или tun-модуль может быть не включен. Для прошивки CM9 можете попробовать исправить владельца прямо из настроек программы + Открытие интерфейса tun окончилось неудачей + "Ошибка: " + Очистить + информация + Показать подробности о подключении + Последняя используемая конфигурация OpenVPN: + Адрес IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS-сервер: %s + DNS-домен: %s + Маршруты: %s + Маршруты IPv6: %s + Получена информация интерфейса %1$s и %2$s, второй адрес является удалённым адресом канала. Используется сетевая маска /32 для локального IP адреса. Режим, установленный OpenVPN: \"%3$s\". + Невозможно использовать выражения %1$s и %2$s как маршрут по стандарту CIDR. используется /32 как маска подсети. + Маршрут исправлен с %1$s/%2$s на %3$s/%2$s + Не удается получить доступ к хранилищу ключей и сертификатов Android. Это может быть вызвано обновлением прошивки или восстановления старой копии приложения или его настроек. Пожалуйста, отредактируйте профиль VPN и заново укажите ключи и сертификаты в разделе Основные параметры. + %1$s %2$s + Отправить файл журнала + Отправить + LEAP Android файла лога + Скопировать лог в буфер обмена + Ваша прошивка не поддерживает VPNService API, извините :( + Отказ в открытии устройства tun без информации об IP-адресе + Ожидание сообщения о состоянии… + импортируемый профиль + импортируемый профиль %d + Имя пользователя не должно быть пустым. + Файл PKCS12-ключа + Пароль закрытого ключа + Пароль + Создание конфигурации… + Получен сертификат \'%s\' из хранилища ключей + Статус сети: %s + Не удалось получить CA из хранилища ключей Android. Аутентификация не удалась. + Работает на %1$s (%2$s) %3$s, Android API %4$d + Ошибка подписи с использованием ключа из хранилища Android %1$s: %2$s + Русский перевод от RusFox <horonitel@gmail.com> + DNS-серверы не используются. Разрешение имен может не работать. Рассмотрите возможность указания DNS-серверов + Не удалось добавить DNS-сервер \"%1$s\", отклонен системой: %2$s + Ошибка при получении параметров прокси-сервера: %s + Используется прокси-сервер %1$s %2$d + Игнорировать + Перезагрузка + Изменения конфигурации применяются после перезапуска VPN. (Пере)запустить VPN теперь? + Конфигурация изменена + + OpenVPN завершилась неожиданно. Пожалуйста, посмотрите опцию \"Отправить Minidump\" в главном меню + Bitmask - %s + + Подключение + Ожидание ответа сервера + Проверка подлинности + Получение конфигурации клиента + Назначение IP-адресов + Добавление маршрутов + Подключено + Повторное подключение + Выход + Не запущено + Разрешение имен узлов + Подключение (TCP) + Ошибка аутентификации + Ожидание работы сети + 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 @@ + + + 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 @@ + + + Початковий код і відстеження проблем доступні по https://github.com/leapcode/bitmask_android/ + Ця програма використовує такі компоненти; перегляньте вихідний код для повної інформації про ліцензії + Про + Помилок не знайдено + Помилка конфігурації + Помилка при розборі адреси IPv4 + Помилка аналізу налаштованих маршрутів + Підключення до VPN + Профіль, вказаний у ярлику, не знайдено + Маршрут відхилено Андроїдом + Від\'єднати + очистити журнал + Підтвердження скасування + Відключення активних VPN/скасувати спробу підключення? + Змінити налаштування VPN + На деяких користувацьких прошивках ICS права на /dev/tun можуть бути невірними або модуль tun може бути взагалі відсутнім. + Не вдалося відкрити інтерфейс tun + "Помилка: " + Очистити + Інформація + Показати відомості про підключення + Останній інтерфейс конфігурації з OpenVPN: + Адреса IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS-сервер: %s + DNS домен: %s + Маршрути: %s + Маршрути IPv6: %s + Отримано інформацію інтерфейсу %1$s і %2$s, друга адреса є віддаленою адресою каналу. Використовується мережева маска /32 для локальної IP-адреси. Режим, встановлений OpenVPN: \"%3$s\". + Неможливо використовувати вирази %1$s і %2$s як маршрут за стандартом CIDR. використовується /32 як маска підмережі. + Виправлено маршрут з %1$s/%2$s на %3$s/%2$s + Не можна отримати доступ до сховища ключів та сертифікатів Андроїд. Це можливо спричинено оновлення прошивки або відновленням резервної копії програми чи її налаштувань. Будь ласка, відредагуйте профіль VPN та заново виберіть сертифікат у основних параметрах для створення доступу до сертифікату. + %1$s %2$s + Надіслати файл журналу + Надіслати + LEAP Android файл журналу + Скопійовано запис журналу до буферу обміну + Ваша прошивка не підтримує VPNService API, вибачте :( + Відмова у відкритті пристрою tun без інформації про IP-адресу + Очікування повідомлення стану... + імпортований профіль + імпортований профіль: %d + Ім\'я користувача не може бути порожнім. + PKCS12 Ключ шифрування файлу + Пароль закритого ключа + Пароль + Побудова конфігурації… + Отримано сертифікат \'%s\' з сховища ключів + Статус мережі: %s + Не вдалося отримати СА сертифікат при читанні із сховища ключів Андроїд. Автентифікація не вдалася. + Працює на %1$s (%2$s) %3$s, Android API %4$d + Помилка підпису з використанням ключа із сховища Андроїд %1$s: %2$s + Переклад українською від wvolov + Жодний DNS сервер не використовується. Розширення імен можуть не працювати. Розгляньте можливість вказання DNS серверів + Не можливо додати DNS-сервер \"%1$s\", відхилено системою: %2$s + Помилка отримання параметрів проксі: %s + Використовується проксі %1$s %2$d + Ігнорувати + Перезапустити + Після перезапуску VPN застосувати зміни конфігурації. (Пере)запустити VPN тепер? + Конфігурація змінена + + OpenVPN впав несподівано. Будь ласка, розгляньте використання параметру \"Надіслати Мінідамп\" в головному меню + Bitmask - %s + + Підключення + Очікування відповіді сервера + Аутентифікація + Отримання конфігурації клієнта + Перепризначення IP-адрес + Додавання маршрутів + Підключено + Повторне підключення + Виходимо + Не працює + Розпізнавання імен вузлів + Підключення (TCP) + Помилка автентифікації + Очікування на використання мережі + 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 @@ + + + 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 @@ + + + 请前往 https://github.com/leapcode/bitmask_android/ 源码或提供问题反馈 + 本程序使用以下组件,请在 Licenses 查看源码获取更详细内容。 + 关于 + 未找到错误 + 配置有错误 + 无法解析 IPv4 地址 + 无法解析自定义路由 + 连接到 VPN + 未找到快捷方式中指定的配置文件 + Android 拒绝了路由 + 断开 + 清除日志 + 取消确认 + 断开已连接的 VPN / 取消连接尝试? + 编辑 VPN 设置 + 未能打开 tun 模块 + 错误 + 清除 + 信息 + 显示连接信息 + 最后 OpenVPN 接口配置: + 本地 IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS 服务器: %s + DNS 域: %s + IPv4 路由: %s + IPv6 路由: %s + %1$s %2$s + 发送日志文件 + 发送 + LEAP Android 日志文件 + 日志条目已复制剪贴板 + 无 IP 信息,拒绝打开 tun 设备 + 等待状态消息 + 已导入配置文件 + 已导入配置文件 %d + 用户名不能为空。 + PKCS12 文件加密密钥 + 私钥密码 + 密码 + 正在生成配置 + 网络状态: %s + 运行 %1$s ( %2$s ) %3$s ,Android API %4$d + 中文翻译: 白达卫 +<59539051+ovpntrans.zh@mail.dcu.ie> + 获取代理设置时出错:%s + 使用代理 %1$s %2$d + 忽略 + 重启 + 配置已更改 + 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 @@ + + + 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 @@ + + + 取得原始碼與個案追蹤,可上 https://github.com/leapcode/bitmask_android/ + 本程序使用了以下元件,其作者和授權資訊如下 + 關於 + 未有找到錯誤 + 設定中含有錯誤 + 解析IPv4地址時發生錯誤 + 解析自訂路由時發生錯誤 + 連接到VPN + 在快捷方式找不到指定的設定檔 + 路由被Android拒絕 + 斷線 + 清除記錄檔 + 確認取消 + 編輯VPN設定 + 一些自訂的Android4.0 ROM存在/dev/tun的擁有者權限問題,甚至完全沒有Tun模組。CM9用家請嘗試於\"全域設置\"下修正Tun擁有者。 + 無法開啟Tun網絡介面 + "錯誤: " + 清除 + 資訊 + 顯示連線的詳細資訊 + 本地IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS伺服器: %s + DNS網域: %s + 路徑: %s + IPv6路由: %s + %1$s %2$s + 分享記錄檔 + 分享 + LEAP Android 記錄檔 + 已將記錄複製到剪貼簿 + 你的Android ROM不支援VPN服務API,抱歉了。 :( + 等待狀態訊息... + 使用者名稱不能為空。 + PKCS12檔加密金鑰 + 私密金鑰密碼 + 密碼 + 正在生成設定檔… + 網絡狀態: %s + 於 %1$s (%2$s) %3$s 上運行, Android API 版本: %4$d + 繁體中文 由 羊羊@自由網絡研究中心 <sora8964@gmail.com> 翻譯 + 沒有任何DNS伺服器可用,可能無法進行網域名稱解析。請考慮設置自訂的DNS伺服器 + 取得代理伺服器資訊時發生錯誤: %s + 使用代理伺服器 %1$s %2$d + 忽略 + 重置 + 配置變更只會在重新啟動VPN時才生效,現在要(重新)啟動VPN嗎? + 設定已變更 + + OpenVPN非預期地崩潰,你或者會考慮在主選單下傳送Minidump給開發人員。 + 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 @@ + + + + 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 @@ + + + + + + + + \ 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..492c48c7 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,142 @@ + + + + Cancel + OK + Retry + Source code and issue tracker available at https://github.com/leapcode/bitmask_android/ + Translations welcome and appreciated. See our Transifex project at https://www.transifex.com/projects/p/bitmask-android/ + This program uses the following components; see the source code for full details on the licenses + About Bitmask + Switch provider + No error found + Error in Configuration + Error parsing the IPv4 address + Error parsing the custom routes + Connect to VPN + Profile specified in shortcut not found + Route rejected by Android + Disconnect + clear log + Cancel Confirmation + Disconnect the connected VPN/cancel the connection attempt? + Edit VPN Settings + On some custom ICS images the permission on /dev/tun might be wrong, or the tun module might be missing completely. + Failed to open the tun interface + "Error: " + Clear + info + Show connection details + Last interface configuration from OpenVPN: + Local IPv4: %1$s/%2$d IPv6: %3$s MTU: %4$d + DNS Server: %s + DNS Domain: %s + Routes: %s + Routes IPv6: %s + 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\". + Cannot make sense of %1$s and %2$s as IP route with CIDR netmask, using /32 as netmask. + Corrected route %1$s/%2$s to %3$s/%2$s + 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. + %1$s %2$s + Send log file + Send + Bitmask OpenVPN log file + Copied log entry to clip board + Your image does not support the VPNService API, sorry :( + Refusing to open tun device without IP information + Waiting for state message… + imported profile + imported profile %d + The username must not be empty. + PKCS12 File Encryption Key + Private Key Password + Password + Building configuration… + Got certificate \'%s\' from Keystore + Network Status: %s + No CA Certificate returned while reading from Android keystore. Authentication will probably fail. + Running on %1$s (%2$s) %3$s, Android API %4$d + Error signing with Android keystore key %1$s: %2$s + English translation by Arne Schwabe <arne@rfc2549.org> + No DNS servers being used. Name resolution may not work. Consider setting custom DNS Servers + Could not add DNS Server \"%1$s\", rejected by the system: %2$s + Error getting proxy settings: %s + Using proxy %1$s %2$d + Ignore + Restart + Configuration changes are applied after restarting the VPN. (Re)start the VPN now? + Configuration changed + OpenVPN crashed unexpectedly. Please consider using the send Minidump option in the main menu + Bitmask - %s + Connecting + Waiting for server reply + Authenticating + Getting client configuration + Assigning IP addresses + Adding routes + Connected + Reconnecting + Exiting + Not running + Resolving host names + Connecting (TCP) + Authentication failed + Waiting for usable network + Settings + Bitmask + Provider: + No provider configured + Access EIP connection settings + Status unknown. + Connection will be secure using an anonymous certificate. + Connection secure using an anonymous certificate. + Connection will be secure using your own certificate. + Connection secure using your own certificate. + Encrypted Internet + Select a service provider + Add new Provider + Add a new service provider + Save + Domain name + It seems your URL is well formed + It seems your URL is not well formed + Provider details + Use anonymously + username + Please enter your username + password + User message + About Bitmask" + Try again: server math error. + Incorrect username or password. + It should have at least 8 characters. + Try again: Client HTTP error + Try again: I/O error + Try again: Bad response from the server + Update the app + Log In + Log Out + Configuration Error + Configure + Exit + 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. + Server is unreachable, please try again. + It doesn\'t seem to be a Bitmask provider. + This is not a trusted Bitmask provider. + Configuring provider + Your anon cert was not downloaded + Logging in + Logging out from this session. + Didn\'t logged out. + Authentication succeeded. + Authentication failed. + Your own cert has been correctly downloaded. + Your own cert has incorrectly been downloaded. + Initiating connection + Cancel connection? + There is a connection attempt in progress. Do you wish to cancel it? + Yes + No + "Not running! Connection not secure!" + Connection Secure. + 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 @@ + + + + + + + + + + + + + + + + \ 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 @@ + + + Bitmask + Bitmask + + Copyright 2012\nLEAP Encryption Access Project <info@leap.se> + Copyright © 2002–2010 OpenVPN Technologies, Inc. <sales@openvpn.net>\n + + "OpenVPN" is a trademark of OpenVPN Technologies, Inc. + Copyright © 1996 – 2011 Markus Franz Xaver Johannes Oberhumer + 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. + OpenVPN + LZO + OpenSSL + Unknown state + -- cgit v1.2.3