summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient/providersetup
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/providersetup')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java128
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java86
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java98
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java946
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java84
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java21
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java272
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java57
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java21
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java189
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java43
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java109
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java125
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java46
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java289
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java121
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java32
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java479
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java193
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java240
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java55
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java39
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java182
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java158
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java361
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java26
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java42
27 files changed, 4442 insertions, 0 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
new file mode 100644
index 00000000..23c750a3
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2017 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import androidx.annotation.NonNull;
+import androidx.core.app.JobIntentService;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+
+/**
+ * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}})
+ * used to manage communications with the provider server.
+ * <p/>
+ * It's an JobIntentService because it downloads data from the Internet, so it operates in the background.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ * @author cyberta
+ */
+
+public class ProviderAPI extends JobIntentService implements ProviderApiManagerBase.ProviderApiServiceCallback {
+
+ /**
+ * Unique job ID for this service.
+ */
+ static final int JOB_ID = 161375;
+
+ final public static String
+ TAG = ProviderAPI.class.getSimpleName(),
+ SET_UP_PROVIDER = "setUpProvider",
+ UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
+ DOWNLOAD_GEOIP_JSON = "downloadGeoIpJson",
+ SIGN_UP = "srpRegister",
+ LOG_IN = "srpAuth",
+ LOG_OUT = "logOut",
+ DOWNLOAD_VPN_CERTIFICATE = "downloadUserAuthedVPNCertificate",
+ UPDATE_INVALID_VPN_CERTIFICATE = "ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE",
+ PARAMETERS = "parameters",
+ RECEIVER_KEY = "receiver",
+ ERRORS = "errors",
+ ERRORID = "errorId",
+ BACKEND_ERROR_KEY = "error",
+ BACKEND_ERROR_MESSAGE = "message",
+ USER_MESSAGE = "userMessage",
+ DOWNLOAD_SERVICE_JSON = "ProviderAPI.DOWNLOAD_SERVICE_JSON";
+
+ final public static int
+ SUCCESSFUL_LOGIN = 3,
+ FAILED_LOGIN = 4,
+ SUCCESSFUL_SIGNUP = 5,
+ FAILED_SIGNUP = 6,
+ SUCCESSFUL_LOGOUT = 7,
+ LOGOUT_FAILED = 8,
+ CORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 9,
+ INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE = 10,
+ PROVIDER_OK = 11,
+ PROVIDER_NOK = 12,
+ CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
+ INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14,
+ CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 15,
+ INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE = 16,
+ CORRECTLY_DOWNLOADED_GEOIP_JSON = 17,
+ INCORRECTLY_DOWNLOADED_GEOIP_JSON = 18;
+
+ ProviderApiManager providerApiManager;
+
+ //TODO: refactor me, please!
+ //used in insecure flavor only
+ @SuppressLint("unused")
+ public static boolean lastDangerOn() {
+ return ProviderApiManager.lastDangerOn();
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ providerApiManager = initApiManager();
+ }
+
+ /**
+ * Convenience method for enqueuing work in to this service.
+ */
+ static void enqueueWork(Context context, Intent work) {
+ try {
+ ProviderAPI.enqueueWork(context, ProviderAPI.class, JOB_ID, work);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected void onHandleWork(@NonNull Intent command) {
+ providerApiManager.handleIntent(command);
+ }
+
+ @Override
+ public void broadcastEvent(Intent intent) {
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ }
+
+ private ProviderApiManager initApiManager() {
+ SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(getResources());
+ return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java
new file mode 100644
index 00000000..79a107d1
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java
@@ -0,0 +1,86 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import se.leap.bitmaskclient.base.models.Constants;
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class ProviderAPICommand {
+ private static final String TAG = ProviderAPICommand.class.getSimpleName();
+ private Context context;
+
+ private String action;
+ private Bundle parameters;
+ private ResultReceiver resultReceiver;
+ private Provider provider;
+
+ private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider, ResultReceiver resultReceiver) {
+ this(context.getApplicationContext(), action, Bundle.EMPTY, provider, resultReceiver);
+ }
+ private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Provider provider) {
+ this(context.getApplicationContext(), action, Bundle.EMPTY, provider);
+ }
+
+ private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider) {
+ this(context.getApplicationContext(), action, parameters, provider, null);
+ }
+
+ private ProviderAPICommand(@NotNull Context context, @NotNull String action, @NotNull Bundle parameters, @NotNull Provider provider, @Nullable ResultReceiver resultReceiver) {
+ super();
+ this.context = context;
+ this.action = action;
+ this.parameters = parameters;
+ this.resultReceiver = resultReceiver;
+ this.provider = provider;
+ }
+
+ private boolean isInitialized() {
+ return context != null;
+ }
+
+ private void execute() {
+ if (isInitialized()) {
+ Intent intent = setUpIntent();
+ ProviderAPI.enqueueWork(context, intent);
+ }
+ }
+
+ private Intent setUpIntent() {
+ Intent command = new Intent(context, ProviderAPI.class);
+
+ command.setAction(action);
+ command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ if (resultReceiver != null) {
+ command.putExtra(ProviderAPI.RECEIVER_KEY, resultReceiver);
+ }
+ command.putExtra(Constants.PROVIDER_KEY, provider);
+
+ return command;
+ }
+
+ public static void execute(Context context, String action, @NotNull Provider provider) {
+ ProviderAPICommand command = new ProviderAPICommand(context, action, provider);
+ command.execute();
+ }
+
+ public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider) {
+ ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider);
+ command.execute();
+ }
+
+ public static void execute(Context context, String action, Bundle parameters, @NotNull Provider provider, ResultReceiver resultReceiver) {
+ ProviderAPICommand command = new ProviderAPICommand(context, action, parameters, provider, resultReceiver);
+ command.execute();
+ }
+
+ public static void execute(Context context, String action, @NotNull Provider provider, ResultReceiver resultReceiver) {
+ ProviderAPICommand command = new ProviderAPICommand(context, action, provider, resultReceiver);
+ command.execute();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java
new file mode 100644
index 00000000..ba902566
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.providersetup;
+
+import androidx.annotation.NonNull;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Locale;
+import java.util.Scanner;
+
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+
+/**
+ * Created by cyberta on 08.01.18.
+ */
+
+public class ProviderApiConnector {
+
+ private static final MediaType JSON
+ = MediaType.parse("application/json; charset=utf-8");
+
+
+ public static boolean delete(OkHttpClient okHttpClient, String deleteUrl) {
+ try {
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(deleteUrl)
+ .delete();
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ //response code 401: already logged out
+ if (response.isSuccessful() || response.code() == 401) {
+ return true;
+ }
+ } catch (IOException | RuntimeException e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException {
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method("GET", null);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ return response.isSuccessful();
+
+ }
+
+ public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException {
+
+ RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method(request_method, jsonBody);
+ for (Pair<String, String> keyValPair : headerArgs) {
+ requestBuilder.addHeader(keyValPair.first, keyValPair.second);
+ }
+
+ //TODO: move to getHeaderArgs()?
+ String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
+ requestBuilder.addHeader("Accept-Language", locale);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ InputStream inputStream = response.body().byteStream();
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ return scanner.next();
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
new file mode 100644
index 00000000..8a0c8f02
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java
@@ -0,0 +1,946 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.net.UnknownServiceException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.base.models.Constants.CREDENTIAL_ERRORS;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.providersetup.connectivity.OkHttpClientGenerator;
+import se.leap.bitmaskclient.providersetup.models.LeapSRPSession;
+import se.leap.bitmaskclient.providersetup.models.SrpCredentials;
+import se.leap.bitmaskclient.providersetup.models.SrpRegistrationData;
+import se.leap.bitmaskclient.base.utils.ConfigHelper;
+
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD;
+import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME;
+import static se.leap.bitmaskclient.base.models.Constants.EIP_ACTION_START;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_PRIVATE_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.base.models.Provider.CA_CERT;
+import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.BACKEND_ERROR_MESSAGE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_SERVICE_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_LOGIN;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.FAILED_SIGNUP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_GEOIP_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOGOUT_FAILED;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_IN;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_OUT;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PARAMETERS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.RECEIVER_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGIN;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_LOGOUT;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SUCCESSFUL_SIGNUP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_INVALID_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
+import static se.leap.bitmaskclient.R.string.certificate_error;
+import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_json_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
+import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.server_unreachable_message;
+import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.vpn_certificate_is_invalid;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
+import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getFingerprintFromCertificate;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.parseRsaKeyFromString;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.deleteProviderDetailsFromPreferences;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getFromPersistedProvider;
+
+/**
+ * Implements the logic of the http api calls. The methods of this class needs to be called from
+ * a background thread.
+ */
+
+public abstract class ProviderApiManagerBase {
+
+ private final static String TAG = ProviderApiManagerBase.class.getName();
+
+ public interface ProviderApiServiceCallback {
+ void broadcastEvent(Intent intent);
+ }
+
+ private ProviderApiServiceCallback serviceCallback;
+
+ protected SharedPreferences preferences;
+ protected Resources resources;
+ OkHttpClientGenerator clientGenerator;
+
+ ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ this.preferences = preferences;
+ this.resources = resources;
+ this.serviceCallback = callback;
+ this.clientGenerator = clientGenerator;
+ }
+
+ public void handleIntent(Intent command) {
+// Log.d(TAG, "handleIntent was called!");
+ ResultReceiver receiver = null;
+ if (command.getParcelableExtra(RECEIVER_KEY) != null) {
+ receiver = command.getParcelableExtra(RECEIVER_KEY);
+ }
+ String action = command.getAction();
+ Bundle parameters = command.getBundleExtra(PARAMETERS);
+
+ Provider provider = command.getParcelableExtra(PROVIDER_KEY);
+
+ if (provider == null) {
+ //TODO: consider returning error back e.g. NO_PROVIDER
+ Log.e(TAG, action +" called without provider!");
+ return;
+ }
+ if (action == null) {
+ Log.e(TAG, "Intent without action sent!");
+ return;
+ }
+
+ Bundle result = new Bundle();
+ switch (action) {
+ case UPDATE_PROVIDER_DETAILS:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ resetProviderDetails(provider);
+ Bundle task = new Bundle();
+ result = setUpProvider(provider, task);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ getGeoIPJson(provider);
+ sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case SET_UP_PROVIDER:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = setUpProvider(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ getGeoIPJson(provider);
+ sendToReceiverOrBroadcast(receiver, PROVIDER_OK, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, PROVIDER_NOK, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case SIGN_UP:
+ result = tryToRegister(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, SUCCESSFUL_SIGNUP, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, FAILED_SIGNUP, result, provider);
+ }
+ break;
+ case LOG_IN:
+ result = tryToAuthenticate(provider, parameters);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGIN, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, FAILED_LOGIN, result, provider);
+ }
+ break;
+ case LOG_OUT:
+ if (logOut(provider)) {
+ sendToReceiverOrBroadcast(receiver, SUCCESSFUL_LOGOUT, Bundle.EMPTY, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, LOGOUT_FAILED, Bundle.EMPTY, provider);
+ }
+ break;
+ case DOWNLOAD_VPN_CERTIFICATE:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case UPDATE_INVALID_VPN_CERTIFICATE:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = updateVpnCertificate(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, CORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, INCORRECTLY_UPDATED_INVALID_VPN_CERTIFICATE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_SERVICE_JSON:
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ Log.d(TAG, "update eip service json");
+ result = getAndSetEipServiceJson(provider);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_EIP_SERVICE, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ break;
+ case DOWNLOAD_GEOIP_JSON:
+ if (!provider.getGeoipUrl().isDefault()) {
+ boolean startEIP = parameters.getBoolean(EIP_ACTION_START);
+ ProviderObservable.getInstance().setProviderForDns(provider);
+ result = getGeoIPJson(provider);
+ result.putBoolean(EIP_ACTION_START, startEIP);
+ if (result.getBoolean(BROADCAST_RESULT_KEY)) {
+ sendToReceiverOrBroadcast(receiver, CORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
+ } else {
+ sendToReceiverOrBroadcast(receiver, INCORRECTLY_DOWNLOADED_GEOIP_JSON, result, provider);
+ }
+ ProviderObservable.getInstance().setProviderForDns(null);
+ }
+ }
+ }
+
+ void resetProviderDetails(Provider provider) {
+ provider.reset();
+ deleteProviderDetailsFromPreferences(preferences, provider.getDomain());
+ }
+
+ String formatErrorMessage(final int errorStringId) {
+ return formatErrorMessage(getProviderFormattedString(resources, errorStringId));
+ }
+
+ private String formatErrorMessage(String errorMessage) {
+ return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }";
+ }
+
+ private JSONObject getErrorMessageAsJson(final int toastStringId) {
+ try {
+ return new JSONObject(formatErrorMessage(toastStringId));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return new JSONObject();
+ }
+ }
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ jsonObject.put(ERRORID, errorId);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private Bundle tryToRegister(Provider provider, Bundle task) {
+ Bundle result = new Bundle();
+
+ String username = task.getString(CREDENTIALS_USERNAME);
+ String password = task.getString(CREDENTIALS_PASSWORD);
+
+ if(provider == null) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ Log.e(TAG, "no provider when trying to register");
+ return result;
+ }
+
+ if (validUserLoginData(username, password)) {
+ result = register(provider, username, password);
+ } else {
+ if (!wellFormedPassword(password)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ result.putString(CREDENTIALS_USERNAME, username);
+ result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true);
+ }
+ if (!validUsername(username)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true);
+ }
+ }
+
+ return result;
+ }
+
+ private Bundle register(Provider provider, String username, String password) {
+ JSONObject stepResult = null;
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
+ if (okHttpClient == null) {
+ return backendErrorNotification(stepResult, username);
+ }
+
+ LeapSRPSession client = new LeapSRPSession(username, password);
+ byte[] salt = client.calculateNewSalt();
+
+ BigInteger password_verifier = client.calculateV(username, password, salt);
+
+ JSONObject api_result = sendNewUserDataToSRPServer(provider.getApiUrlWithVersion(), username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
+
+ Bundle result = new Bundle();
+ if (api_result.has(ERRORS) || api_result.has(BACKEND_ERROR_KEY))
+ result = backendErrorNotification(api_result, username);
+ else {
+ result.putString(CREDENTIALS_USERNAME, username);
+ result.putString(CREDENTIALS_PASSWORD, password);
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ }
+
+ return result;
+ }
+
+ /**
+ * Starts the authentication process using SRP protocol.
+ *
+ * @param task containing: username, password and provider
+ * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if authentication was successful.
+ */
+ private Bundle tryToAuthenticate(Provider provider, Bundle task) {
+ Bundle result = new Bundle();
+
+ String username = task.getString(CREDENTIALS_USERNAME);
+ String password = task.getString(CREDENTIALS_PASSWORD);
+
+ if (validUserLoginData(username, password)) {
+ result = authenticate(provider, username, password);
+ } else {
+ if (!wellFormedPassword(password)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ result.putString(CREDENTIALS_USERNAME, username);
+ result.putBoolean(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString(), true);
+ }
+ if (!validUsername(username)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ result.putBoolean(CREDENTIAL_ERRORS.USERNAME_MISSING.toString(), true);
+ }
+ }
+
+ return result;
+ }
+
+ private Bundle authenticate(Provider provider, String username, String password) {
+ Bundle result = new Bundle();
+ JSONObject stepResult = new JSONObject();
+
+ String providerApiUrl = provider.getApiUrlWithVersion();
+
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), stepResult);
+ if (okHttpClient == null) {
+ return backendErrorNotification(stepResult, username);
+ }
+
+ LeapSRPSession client = new LeapSRPSession(username, password);
+ byte[] A = client.exponential();
+
+ JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient);
+ try {
+ String salt = step_result.getString(LeapSRPSession.SALT);
+ byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray();
+ byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
+ if (M1 != null) {
+ step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient);
+ setTokenIfAvailable(step_result);
+ byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray();
+ if (client.verify(M2)) {
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ } else {
+ backendErrorNotification(step_result, username);
+ }
+ } else {
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ result.putString(CREDENTIALS_USERNAME, username);
+ result.putString(USER_MESSAGE, resources.getString(R.string.error_srp_math_error_user_message));
+ }
+ } catch (JSONException e) {
+ result = backendErrorNotification(step_result, username);
+ e.printStackTrace();
+ }
+
+ return result;
+ }
+
+ private boolean setTokenIfAvailable(JSONObject authentication_step_result) {
+ try {
+ LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN));
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ private Bundle backendErrorNotification(JSONObject result, String username) {
+ Bundle userNotificationBundle = new Bundle();
+ if (result.has(ERRORS)) {
+ Object baseErrorMessage = result.opt(ERRORS);
+ if (baseErrorMessage instanceof JSONObject) {
+ try {
+ JSONObject errorMessage = result.getJSONObject(ERRORS);
+ String errorType = errorMessage.keys().next().toString();
+ String message = errorMessage.get(errorType).toString();
+ userNotificationBundle.putString(USER_MESSAGE, message);
+ } catch (JSONException | NoSuchElementException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ } else if (baseErrorMessage instanceof String) {
+ try {
+ String errorMessage = result.getString(ERRORS);
+ userNotificationBundle.putString(USER_MESSAGE, errorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+ } else if (result.has(BACKEND_ERROR_KEY)) {
+ try {
+ String backendErrorMessage = resources.getString(R.string.error_json_exception_user_message);
+ if (result.has(BACKEND_ERROR_MESSAGE)) {
+ backendErrorMessage = resources.getString(R.string.error) + result.getString(BACKEND_ERROR_MESSAGE);
+ }
+ userNotificationBundle.putString(USER_MESSAGE, backendErrorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ if (!username.isEmpty())
+ userNotificationBundle.putString(CREDENTIALS_USERNAME, username);
+ userNotificationBundle.putBoolean(BROADCAST_RESULT_KEY, false);
+
+ return userNotificationBundle;
+ }
+
+ private void sendToReceiverOrBroadcast(ResultReceiver receiver, int resultCode, Bundle resultData, Provider provider) {
+ if (resultData == null || resultData == Bundle.EMPTY) {
+ resultData = new Bundle();
+ }
+ resultData.putParcelable(PROVIDER_KEY, provider);
+ if (receiver != null) {
+ receiver.send(resultCode, resultData);
+ } else {
+ broadcastEvent(resultCode, resultData);
+ }
+ }
+
+ private void broadcastEvent(int resultCode , Bundle resultData) {
+ Intent intentUpdate = new Intent(BROADCAST_PROVIDER_API_EVENT);
+ intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
+ intentUpdate.putExtra(BROADCAST_RESULT_CODE, resultCode);
+ intentUpdate.putExtra(BROADCAST_RESULT_KEY, resultData);
+ serviceCallback.broadcastEvent(intentUpdate);
+ }
+
+
+ /**
+ * Validates parameters entered by the user to log in
+ *
+ * @param username
+ * @param password
+ * @return true if both parameters are present and the entered password length is greater or equal to eight (8).
+ */
+ private boolean validUserLoginData(String username, String password) {
+ return validUsername(username) && wellFormedPassword(password);
+ }
+
+ private boolean validUsername(String username) {
+ return username != null && !username.isEmpty();
+ }
+
+ /**
+ * Validates a password
+ *
+ * @param password
+ * @return true if the entered password length is greater or equal to eight (8).
+ */
+ private boolean wellFormedPassword(String password) {
+ return password != null && password.length() >= 8;
+ }
+
+ /**
+ * Sends an HTTP POST request to the authentication server with the SRP Parameter A.
+ *
+ * @param server_url
+ * @param username
+ * @param clientA First SRP parameter sent
+ * @param okHttpClient
+ * @return response from authentication server
+ */
+ private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) {
+ SrpCredentials srpCredentials = new SrpCredentials(username, clientA);
+ return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient);
+ }
+
+ /**
+ * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M).
+ *
+ * @param server_url
+ * @param username
+ * @param m1 Second SRP parameter sent
+ * @param okHttpClient
+ * @return response from authentication server
+ */
+ private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) {
+ String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}";
+ return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient);
+ }
+
+ /**
+ * Sends an HTTP POST request to the api server to register a new user.
+ *
+ * @param server_url
+ * @param username
+ * @param salt
+ * @param password_verifier
+ * @param okHttpClient
+ * @return response from authentication server
+ */
+ private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) {
+ return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient);
+ }
+
+ /**
+ * Executes an HTTP request expecting a JSON response.
+ *
+ * @param url
+ * @param request_method
+ * @return response from authentication server
+ */
+ private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) {
+ return requestJsonFromServer(url, request_method, jsonString, new ArrayList<Pair<String, String>>(), okHttpClient);
+ }
+
+ protected String sendGetStringToServer(@NonNull String url, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient);
+ }
+
+
+
+ private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ JSONObject responseJson;
+ String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
+
+ try {
+ responseJson = new JSONObject(plain_response);
+ } catch (NullPointerException | JSONException e) {
+ e.printStackTrace();
+ responseJson = getErrorMessageAsJson(error_json_exception_user_message);
+ }
+ return responseJson;
+
+ }
+
+ private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ String plainResponseBody;
+
+ try {
+
+ plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
+
+ } catch (NullPointerException npe) {
+ plainResponseBody = formatErrorMessage(error_json_exception_user_message);
+ } catch (UnknownHostException | SocketTimeoutException e) {
+ plainResponseBody = formatErrorMessage(server_unreachable_message);
+ } catch (MalformedURLException e) {
+ plainResponseBody = formatErrorMessage(malformed_url);
+ } catch (SSLHandshakeException | SSLPeerUnverifiedException e) {
+ plainResponseBody = formatErrorMessage(certificate_error);
+ } catch (ConnectException e) {
+ plainResponseBody = formatErrorMessage(service_is_down_error);
+ } catch (IllegalArgumentException e) {
+ plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message);
+ } catch (UnknownServiceException e) {
+ //unable to find acceptable protocols - tlsv1.2 not enabled?
+ plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message);
+ } catch (IOException e) {
+ plainResponseBody = formatErrorMessage(error_io_exception_user_message);
+ }
+
+ return plainResponseBody;
+ }
+
+ private boolean canConnect(Provider provider, Bundle result) {
+ JSONObject errorJson = new JSONObject();
+ String providerUrl = provider.getApiUrlString() + "/provider.json";
+
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), errorJson);
+ if (okHttpClient == null) {
+ result.putString(ERRORS, errorJson.toString());
+ return false;
+ }
+
+ try {
+
+ return ProviderApiConnector.canConnect(okHttpClient, providerUrl);
+
+ } catch (UnknownHostException | SocketTimeoutException e) {
+ setErrorResult(result, server_unreachable_message, null);
+ } catch (MalformedURLException e) {
+ setErrorResult(result, malformed_url, null);
+ } catch (SSLHandshakeException e) {
+ setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ } catch (ConnectException e) {
+ setErrorResult(result, service_is_down_error, null);
+ } catch (IllegalArgumentException e) {
+ setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (UnknownServiceException e) {
+ //unable to find acceptable protocols - tlsv1.2 not enabled?
+ setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (IOException e) {
+ setErrorResult(result, error_io_exception_user_message, null);
+ }
+ return false;
+ }
+
+ /**
+ * Downloads a provider.json from a given URL, adding a new provider using the given name.
+ *
+ * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider
+ * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the update was successful.
+ */
+ protected abstract Bundle setUpProvider(Provider provider, Bundle task);
+
+ /**
+ * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways
+ * @return a bundle with a boolean value mapped to a key named BROADCAST_RESULT_KEY, and which is true if the download was successful.
+ */
+ protected abstract Bundle getAndSetEipServiceJson(Provider provider);
+
+ /**
+ * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate.
+ *
+ * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error.
+ */
+ protected abstract Bundle updateVpnCertificate(Provider provider);
+
+
+ /**
+ * Fetches the Geo ip Json, containing a list of gateways sorted by distance from the users current location
+ *
+ * @param provider
+ * @return
+ */
+ protected abstract Bundle getGeoIPJson(Provider provider);
+
+
+ protected boolean isValidJson(String jsonString) {
+ try {
+ new JSONObject(jsonString);
+ return true;
+ } catch(JSONException e) {
+ return false;
+ } catch(NullPointerException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ protected boolean validCertificate(Provider provider, String certString) {
+ boolean result = false;
+ if (!ConfigHelper.checkErroneousDownload(certString)) {
+ X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certString);
+ try {
+ if (certificate != null) {
+ JSONObject providerJson = provider.getDefinition();
+ String fingerprint = providerJson.getString(Provider.CA_CERT_FINGERPRINT);
+ String encoding = fingerprint.split(":")[0];
+ String expectedFingerprint = fingerprint.split(":")[1];
+ String realFingerprint = getFingerprintFromCertificate(certificate, encoding);
+
+ result = realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim());
+ } else
+ result = false;
+ } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) {
+ result = false;
+ }
+ }
+
+ return result;
+ }
+
+ protected void getPersistedProviderUpdates(Provider provider) {
+ String providerDomain = getDomainFromMainURL(provider.getMainUrlString());
+ if (hasUpdatedProviderDetails(providerDomain)) {
+ provider.setCaCert(getPersistedProviderCA(providerDomain));
+ provider.define(getPersistedProviderDefinition(providerDomain));
+ provider.setPrivateKey(getPersistedPrivateKey(providerDomain));
+ provider.setVpnCertificate(getPersistedVPNCertificate(providerDomain));
+ provider.setProviderApiIp(getPersistedProviderApiIp(providerDomain));
+ provider.setProviderIp(getPersistedProviderIp(providerDomain));
+ provider.setGeoipUrl(getPersistedGeoIp(providerDomain));
+ }
+ }
+
+ Bundle validateProviderDetails(Provider provider) {
+ Bundle result = new Bundle();
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+
+ if (!provider.hasDefinition()) {
+ return result;
+ }
+
+ result = validateCertificateForProvider(result, provider);
+
+ //invalid certificate or no certificate
+ if (result.containsKey(ERRORS) || (result.containsKey(BROADCAST_RESULT_KEY) && !result.getBoolean(BROADCAST_RESULT_KEY)) ) {
+ return result;
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+
+ return result;
+ }
+
+ protected Bundle validateCertificateForProvider(Bundle result, Provider provider) {
+ String caCert = provider.getCaCert();
+
+ if (ConfigHelper.checkErroneousDownload(caCert)) {
+ return result;
+ }
+
+ X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(caCert);
+ if (certificate == null) {
+ return setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+ try {
+ certificate.checkValidity();
+ String encoding = provider.getCertificatePinEncoding();
+ String expectedFingerprint = provider.getCertificatePin();
+
+ String realFingerprint = getFingerprintFromCertificate(certificate, encoding);
+ if (!realFingerprint.trim().equalsIgnoreCase(expectedFingerprint.trim())) {
+ return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
+ }
+
+ if (!canConnect(provider, result)) {
+ return result;
+ }
+ } catch (NoSuchAlgorithmException e ) {
+ return setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
+ return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ return result;
+ }
+
+ protected Bundle setErrorResult(Bundle result, String stringJsonErrorMessage) {
+ String reasonToFail = pickErrorMessage(stringJsonErrorMessage);
+ result.putString(ERRORS, reasonToFail);
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+ Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
+ JSONObject errorJson = new JSONObject();
+ String errorMessage = getProviderFormattedString(resources, errorMessageId);
+ if (errorId != null) {
+ addErrorMessageToJson(errorJson, errorMessage, errorId);
+ } else {
+ addErrorMessageToJson(errorJson, errorMessage);
+ }
+ result.putString(ERRORS, errorJson.toString());
+ result.putBoolean(BROADCAST_RESULT_KEY, false);
+ return result;
+ }
+
+ protected String getPersistedPrivateKey(String providerDomain) {
+ return getFromPersistedProvider(PROVIDER_PRIVATE_KEY, providerDomain, preferences);
+ }
+
+ protected String getPersistedVPNCertificate(String providerDomain) {
+ return getFromPersistedProvider(PROVIDER_VPN_CERTIFICATE, providerDomain, preferences);
+ }
+
+ protected JSONObject getPersistedProviderDefinition(String providerDomain) {
+ try {
+ return new JSONObject(getFromPersistedProvider(Provider.KEY, providerDomain, preferences));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return new JSONObject();
+ }
+ }
+
+ protected String getPersistedProviderCA(String providerDomain) {
+ return getFromPersistedProvider(CA_CERT, providerDomain, preferences);
+ }
+
+ protected String getPersistedProviderApiIp(String providerDomain) {
+ return getFromPersistedProvider(PROVIDER_API_IP, providerDomain, preferences);
+ }
+
+ protected String getPersistedProviderIp(String providerDomain) {
+ return getFromPersistedProvider(PROVIDER_IP, providerDomain, preferences);
+ }
+
+ protected String getPersistedGeoIp(String providerDomain) {
+ return getFromPersistedProvider(GEOIP_URL, providerDomain, preferences);
+ }
+
+ protected boolean hasUpdatedProviderDetails(String domain) {
+ return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(CA_CERT + "." + domain);
+ }
+
+ protected String getDomainFromMainURL(String mainUrl) {
+ return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+
+ }
+
+ /**
+ * Interprets the error message as a JSON object and extract the "errors" keyword pair.
+ * If the error message is not a JSON object, then it is returned untouched.
+ *
+ * @param stringJsonErrorMessage
+ * @return final error message
+ */
+ protected String pickErrorMessage(String stringJsonErrorMessage) {
+ String errorMessage = "";
+ try {
+ JSONObject jsonErrorMessage = new JSONObject(stringJsonErrorMessage);
+ errorMessage = jsonErrorMessage.getString(ERRORS);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ errorMessage = stringJsonErrorMessage;
+ } catch (NullPointerException e) {
+ //do nothing
+ }
+
+ return errorMessage;
+ }
+
+ @NonNull
+ protected List<Pair<String, String>> getAuthorizationHeader() {
+ List<Pair<String, String>> headerArgs = new ArrayList<>();
+ if (!LeapSRPSession.getToken().isEmpty()) {
+ Pair<String, String> authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken());
+ headerArgs.add(authorizationHeaderPair);
+ }
+ return headerArgs;
+ }
+
+ private boolean logOut(Provider provider) {
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(provider.getCaCert(), new JSONObject());
+ if (okHttpClient == null) {
+ return false;
+ }
+
+ String deleteUrl = provider.getApiUrlWithVersion() + "/logout";
+
+ if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) {
+ LeapSRPSession.setToken("");
+ return true;
+ }
+ return false;
+ }
+
+ protected Bundle loadCertificate(Provider provider, String certString) {
+ Bundle result = new Bundle();
+ if (certString == null) {
+ setErrorResult(result, vpn_certificate_is_invalid, null);
+ return result;
+ }
+
+ try {
+ // API returns concatenated cert & key. Split them for OpenVPN options
+ String certificateString = null, keyString = null;
+ String[] certAndKey = certString.split("(?<=-\n)");
+ for (int i = 0; i < certAndKey.length - 1; i++) {
+ if (certAndKey[i].contains("KEY")) {
+ keyString = certAndKey[i++] + certAndKey[i];
+ } else if (certAndKey[i].contains("CERTIFICATE")) {
+ certificateString = certAndKey[i++] + certAndKey[i];
+ }
+ }
+
+ RSAPrivateKey key = parseRsaKeyFromString(keyString);
+ keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
+ provider.setPrivateKey( "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----");
+
+ X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString);
+ certificate.checkValidity();
+ certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT);
+ provider.setVpnCertificate( "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----");
+ result.putBoolean(BROADCAST_RESULT_KEY, true);
+ } catch (CertificateException | NullPointerException e) {
+ e.printStackTrace();
+ setErrorResult(result, vpn_certificate_is_invalid, null);
+ }
+ return result;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
new file mode 100644
index 00000000..710aee0f
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+
+import se.leap.bitmaskclient.base.models.Constants;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState;
+import se.leap.bitmaskclient.providersetup.activities.ProviderListBaseActivity;
+
+/**
+ * Broadcast receiver that handles callback intents of ProviderApi during provider setup.
+ * It is used by CustomProviderSetupActivity for custom branded apps and ProviderListActivity
+ * for 'normal' Bitmask.
+ *
+ * Created by cyberta on 17.08.18.
+ */
+
+public class ProviderApiSetupBroadcastReceiver extends BroadcastReceiver {
+ private WeakReference<ProviderSetupInterface> setupInterfaceRef;
+
+ public ProviderApiSetupBroadcastReceiver(ProviderSetupInterface setupInterface) {
+ this.setupInterfaceRef = new WeakReference<>(setupInterface);
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(ProviderListBaseActivity.TAG, "received Broadcast");
+ ProviderSetupInterface setupInterface = setupInterfaceRef.get();
+ String action = intent.getAction();
+ if (action == null || !action.equalsIgnoreCase(Constants.BROADCAST_PROVIDER_API_EVENT) || setupInterface == null) {
+ return;
+ }
+
+ if (setupInterface.getConfigState() != null &&
+ setupInterface.getConfigState() == ProviderConfigState.SETTING_UP_PROVIDER) {
+ int resultCode = intent.getIntExtra(Constants.BROADCAST_RESULT_CODE, ProviderListBaseActivity.RESULT_CANCELED);
+ Log.d(ProviderListBaseActivity.TAG, "Broadcast resultCode: " + resultCode);
+
+ Bundle resultData = intent.getParcelableExtra(Constants.BROADCAST_RESULT_KEY);
+ Provider handledProvider = resultData.getParcelable(Constants.PROVIDER_KEY);
+
+ if (handledProvider != null && setupInterface.getProvider() != null &&
+ handledProvider.getDomain().equalsIgnoreCase(setupInterface.getProvider().getDomain())) {
+ switch (resultCode) {
+ case ProviderAPI.PROVIDER_OK:
+ setupInterface.handleProviderSetUp(handledProvider);
+ break;
+ case ProviderAPI.PROVIDER_NOK:
+ setupInterface.handleProviderSetupFailed(resultData);
+ break;
+ case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ setupInterface.handleCorrectlyDownloadedCertificate(handledProvider);
+ break;
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ setupInterface.handleIncorrectlyDownloadedCertificate();
+ break;
+ }
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java
new file mode 100644
index 00000000..76ee33f2
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java
@@ -0,0 +1,21 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.view.LayoutInflater;
+
+import com.pedrogomez.renderers.AdapteeCollection;
+import com.pedrogomez.renderers.RendererAdapter;
+import com.pedrogomez.renderers.RendererBuilder;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class ProviderListAdapter extends RendererAdapter<Provider> {
+ public ProviderListAdapter(LayoutInflater layoutInflater, RendererBuilder rendererBuilder,
+ AdapteeCollection<Provider> collection) {
+ super(layoutInflater, rendererBuilder, collection);
+ }
+
+ public void saveProviders() {
+ ProviderManager provider_manager = (ProviderManager) getCollection();
+ provider_manager.saveCustomProvidersToFile();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
new file mode 100644
index 00000000..d33a175f
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java
@@ -0,0 +1,272 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.res.AssetManager;
+import androidx.annotation.VisibleForTesting;
+
+import com.pedrogomez.renderers.AdapteeCollection;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+import static se.leap.bitmaskclient.base.models.Provider.GEOIP_URL;
+import static se.leap.bitmaskclient.base.models.Provider.MAIN_URL;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_API_IP;
+import static se.leap.bitmaskclient.base.models.Provider.PROVIDER_IP;
+import static se.leap.bitmaskclient.base.utils.FileHelper.createFile;
+import static se.leap.bitmaskclient.base.utils.FileHelper.persistFile;
+import static se.leap.bitmaskclient.base.utils.InputStreamHelper.getInputStreamFrom;
+import static se.leap.bitmaskclient.base.utils.InputStreamHelper.loadInputStreamAsString;
+
+/**
+ * Created by parmegv on 4/12/14.
+ */
+public class ProviderManager implements AdapteeCollection<Provider> {
+
+ private AssetManager assetsManager;
+ private File externalFilesDir;
+ private Set<Provider> defaultProviders;
+ private Set<Provider> customProviders;
+ private Set<URL> defaultProviderURLs;
+ private Set<URL> customProviderURLs;
+
+ private static ProviderManager instance;
+
+ final private static String URLS = "urls";
+ final private static String EXT_JSON = ".json";
+ final private static String EXT_PEM = ".pem";
+
+ public static ProviderManager getInstance(AssetManager assetsManager, File externalFilesDir) {
+ if (instance == null)
+ instance = new ProviderManager(assetsManager, externalFilesDir);
+
+ return instance;
+ }
+
+ @VisibleForTesting
+ static void reset() {
+ instance = null;
+ }
+
+ private ProviderManager(AssetManager assetManager, File externalFilesDir) {
+ this.assetsManager = assetManager;
+ addDefaultProviders(assetManager);
+ addCustomProviders(externalFilesDir);
+ }
+
+ private void addDefaultProviders(AssetManager assets_manager) {
+ try {
+ defaultProviders = providersFromAssets(URLS, assets_manager.list(URLS));
+ defaultProviderURLs = getProviderUrlSetFromProviderSet(defaultProviders);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private Set<URL> getProviderUrlSetFromProviderSet(Set<Provider> providers) {
+ HashSet<URL> providerUrls = new HashSet<>();
+ for (Provider provider : providers) {
+ providerUrls.add(provider.getMainUrl().getUrl());
+ }
+ return providerUrls;
+ }
+
+ private Set<Provider> providersFromAssets(String directory, String[] relativeFilePaths) {
+ Set<Provider> providers = new HashSet<>();
+
+ for (String file : relativeFilePaths) {
+ String mainUrl = null;
+ String providerIp = null;
+ String providerApiIp = null;
+ String certificate = null;
+ String providerDefinition = null;
+ String geoipUrl = null;
+ try {
+ String provider = file.substring(0, file.length() - ".url".length());
+ InputStream providerFile = assetsManager.open(directory + "/" + file);
+ mainUrl = extractKeyFromInputStream(providerFile, MAIN_URL);
+ providerIp = extractKeyFromInputStream(providerFile, PROVIDER_IP);
+ providerApiIp = extractKeyFromInputStream(providerFile, PROVIDER_API_IP);
+ geoipUrl = extractKeyFromInputStream(providerFile, GEOIP_URL);
+ certificate = loadInputStreamAsString(assetsManager.open(provider + EXT_PEM));
+ providerDefinition = loadInputStreamAsString(assetsManager.open(provider + EXT_JSON));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ providers.add(new Provider(mainUrl, geoipUrl, providerIp, providerApiIp, certificate, providerDefinition));
+ }
+
+ return providers;
+ }
+
+
+ private void addCustomProviders(File externalFilesDir) {
+ this.externalFilesDir = externalFilesDir;
+ customProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
+ providersFromFiles(externalFilesDir.list()) :
+ new HashSet<>();
+ customProviderURLs = getProviderUrlSetFromProviderSet(customProviders);
+ }
+
+ private Set<Provider> providersFromFiles(String[] files) {
+ Set<Provider> providers = new HashSet<>();
+ try {
+ for (String file : files) {
+ InputStream inputStream = getInputStreamFrom(externalFilesDir.getAbsolutePath() + "/" + file);
+ String mainUrl = extractKeyFromInputStream(inputStream, MAIN_URL);
+ String providerIp = extractKeyFromInputStream(inputStream, PROVIDER_IP);
+ String providerApiIp = extractKeyFromInputStream(inputStream, PROVIDER_API_IP);
+ providers.add(new Provider(mainUrl, providerIp, providerApiIp));
+ }
+ } catch (FileNotFoundException | NullPointerException e) {
+ e.printStackTrace();
+ }
+
+ return providers;
+ }
+
+ private String extractKeyFromInputStream(InputStream inputStream, String key) {
+ String value = "";
+
+ JSONObject fileContents = inputStreamToJson(inputStream);
+ if (fileContents != null)
+ value = fileContents.optString(key);
+ return value;
+ }
+
+ private JSONObject inputStreamToJson(InputStream inputStream) {
+ JSONObject json = null;
+ try {
+ byte[] bytes = new byte[inputStream.available()];
+ if (inputStream.read(bytes) > 0)
+ json = new JSONObject(new String(bytes));
+ inputStream.reset();
+ } catch (IOException | JSONException e) {
+ e.printStackTrace();
+ }
+ return json;
+ }
+
+ public List<Provider> providers() {
+ List<Provider> allProviders = new ArrayList<>();
+ allProviders.addAll(defaultProviders);
+ if(customProviders != null)
+ allProviders.addAll(customProviders);
+ //add an option to add a custom provider
+ //TODO: refactor me?
+ allProviders.add(new Provider());
+ return allProviders;
+ }
+
+ @Override
+ public int size() {
+ return providers().size();
+ }
+
+ @Override
+ public Provider get(int index) {
+ Iterator<Provider> iterator = providers().iterator();
+ while (iterator.hasNext() && index > 0) {
+ iterator.next();
+ index--;
+ }
+ return iterator.next();
+ }
+
+ @Override
+ public boolean add(Provider element) {
+ return element != null &&
+ !defaultProviderURLs.contains(element.getMainUrl().getUrl()) &&
+ customProviders.add(element) &&
+ customProviderURLs.add(element.getMainUrl().getUrl());
+ }
+
+ @Override
+ public boolean remove(Object element) {
+ return element instanceof Provider &&
+ customProviders.remove(element) &&
+ customProviderURLs.remove(((Provider) element).getMainUrl().getUrl());
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends Provider> elements) {
+ Iterator iterator = elements.iterator();
+ boolean addedAll = true;
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ addedAll = customProviders.add(p) &&
+ customProviderURLs.add(p.getMainUrl().getUrl()) &&
+ addedAll;
+ }
+ return addedAll;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> elements) {
+ Iterator iterator = elements.iterator();
+ boolean removedAll = true;
+ try {
+ while (iterator.hasNext()) {
+ Provider p = (Provider) iterator.next();
+ removedAll = ((defaultProviders.remove(p) && defaultProviderURLs.remove(p.getMainUrl().getUrl())) ||
+ (customProviders.remove(p) && customProviderURLs.remove(p.getMainUrl().getUrl()))) &&
+ removedAll;
+ }
+ } catch (ClassCastException e) {
+ return false;
+ }
+
+ return removedAll;
+ }
+
+ @Override
+ public void clear() {
+ defaultProviders.clear();
+ customProviders.clear();
+ customProviderURLs.clear();
+ defaultProviderURLs.clear();
+ }
+
+ void saveCustomProvidersToFile() {
+ try {
+ deleteLegacyCustomProviders();
+
+ for (Provider provider : customProviders) {
+ File providerFile = createFile(externalFilesDir, provider.getName() + EXT_JSON);
+ if (!providerFile.exists()) {
+ persistFile(providerFile, provider.toJson().toString());
+ }
+ }
+ } catch (IOException | SecurityException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Deletes persisted custom providers from from internal storage that are not in customProviders list anymore
+ */
+ private void deleteLegacyCustomProviders() throws IOException, SecurityException {
+ Set<Provider> persistedCustomProviders = externalFilesDir != null && externalFilesDir.isDirectory() ?
+ providersFromFiles(externalFilesDir.list()) : new HashSet<Provider>();
+ persistedCustomProviders.removeAll(customProviders);
+ for (Provider providerToDelete : persistedCustomProviders) {
+ File providerFile = createFile(externalFilesDir, providerToDelete.getName() + EXT_JSON);
+ if (providerFile.exists()) {
+ providerFile.delete();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java
new file mode 100644
index 00000000..52ee4656
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java
@@ -0,0 +1,57 @@
+package se.leap.bitmaskclient.providersetup;
+
+import android.content.*;
+import android.view.*;
+import android.widget.*;
+
+import com.pedrogomez.renderers.*;
+
+import butterknife.*;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.R;
+
+/**
+ * Created by parmegv on 4/12/14.
+ */
+public class ProviderRenderer extends Renderer<Provider> {
+ private final Context context;
+
+ @InjectView(R.id.provider_name)
+ TextView name;
+ @InjectView(R.id.provider_domain)
+ TextView domain;
+
+ public ProviderRenderer(Context context) {
+ this.context = context;
+ }
+
+ @Override
+ protected View inflate(LayoutInflater inflater, ViewGroup parent) {
+ View view = inflater.inflate(R.layout.v_provider_list_item, parent, false);
+ ButterKnife.inject(this, view);
+ return view;
+ }
+
+ @Override
+ protected void setUpView(View rootView) {
+ /*
+ * Empty implementation substituted with the usage of ButterKnife library by Jake Wharton.
+ */
+ }
+
+ @Override
+ protected void hookListeners(View rootView) {
+ //Empty
+ }
+
+ @Override
+ public void render() {
+ Provider provider = getContent();
+ if (!provider.isDefault()) {
+ name.setText(provider.getName());
+ domain.setText(provider.getDomain());
+ } else {
+ domain.setText(R.string.add_provider);
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java
new file mode 100644
index 00000000..7d2b4742
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java
@@ -0,0 +1,21 @@
+package se.leap.bitmaskclient.providersetup;
+
+import com.pedrogomez.renderers.*;
+
+import java.util.*;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+/**
+ * Created by parmegv on 4/12/14.
+ */
+public class ProviderRendererBuilder extends RendererBuilder<Provider> {
+ public ProviderRendererBuilder(Collection<Renderer<Provider>> prototypes) {
+ super(prototypes);
+ }
+
+ @Override
+ protected Class getPrototypeClass(Provider content) {
+ return ProviderRenderer.class;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
new file mode 100644
index 00000000..947d1182
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog.DOWNLOAD_ERRORS.valueOf;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+
+/**
+ * Implements a dialog to show why a download failed.
+ *
+ * @author parmegv
+ */
+public class ProviderSetupFailedDialog extends DialogFragment {
+
+ public static String TAG = "downloaded_failed_dialog";
+ private final static String KEY_PROVIDER = "key provider";
+ private final static String KEY_REASON_TO_FAIL = "key reason to fail";
+ private final static String KEY_DOWNLOAD_ERROR = "key download error";
+ private String reasonToFail;
+ private DOWNLOAD_ERRORS downloadError = DEFAULT;
+
+ private Provider provider;
+
+ /**
+ * Represent error types that need different error handling actions
+ */
+ public enum DOWNLOAD_ERRORS {
+ DEFAULT,
+ ERROR_CORRUPTED_PROVIDER_JSON,
+ ERROR_INVALID_CERTIFICATE,
+ ERROR_CERTIFICATE_PINNING,
+ ERROR_NEW_URL_NO_VPN_PROVIDER
+ }
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(Provider provider, String reasonToFail) {
+ ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog();
+ dialogFragment.reasonToFail = reasonToFail;
+ dialogFragment.provider = provider;
+ return dialogFragment;
+ }
+
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(Provider provider, JSONObject errorJson, boolean testNewURL) {
+ ProviderSetupFailedDialog dialogFragment = new ProviderSetupFailedDialog();
+ dialogFragment.provider = provider;
+ try {
+ if (errorJson.has(ERRORS)) {
+ dialogFragment.reasonToFail = errorJson.getString(ERRORS);
+ } else {
+ //default error msg
+ dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message);
+ }
+
+ if (errorJson.has(ERRORID)) {
+ dialogFragment.downloadError = valueOf(errorJson.getString(ERRORID));
+ } else if (testNewURL) {
+ dialogFragment.downloadError = DOWNLOAD_ERRORS.ERROR_NEW_URL_NO_VPN_PROVIDER;
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ dialogFragment.reasonToFail = dialogFragment.getString(R.string.error_io_exception_user_message);
+ }
+ return dialogFragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ restoreFromSavedInstance(savedInstanceState);
+ }
+
+ @Override
+ @NonNull
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setMessage(reasonToFail)
+ .setNegativeButton(R.string.cancel, (dialog, id)
+ -> interfaceWithConfigurationWizard.cancelSettingUpProvider());
+ switch (downloadError) {
+ case ERROR_CORRUPTED_PROVIDER_JSON:
+ builder.setPositiveButton(R.string.update_provider_details, (dialog, which)
+ -> interfaceWithConfigurationWizard.updateProviderDetails());
+ break;
+ case ERROR_CERTIFICATE_PINNING:
+ case ERROR_INVALID_CERTIFICATE:
+ builder.setPositiveButton(R.string.update_certificate, (dialog, which)
+ -> interfaceWithConfigurationWizard.updateProviderDetails());
+ break;
+ case ERROR_NEW_URL_NO_VPN_PROVIDER:
+ builder.setPositiveButton(R.string.retry, (dialog, id)
+ -> interfaceWithConfigurationWizard.addAndSelectNewProvider(provider.getMainUrlString()));
+ break;
+ default:
+ builder.setPositiveButton(R.string.retry, (dialog, id)
+ -> interfaceWithConfigurationWizard.retrySetUpProvider(provider));
+ break;
+ }
+
+ // Create the AlertDialog object and return it
+ return builder.create();
+ }
+
+ public interface DownloadFailedDialogInterface {
+ void retrySetUpProvider(@NonNull Provider provider);
+
+ void cancelSettingUpProvider();
+
+ void updateProviderDetails();
+
+ void addAndSelectNewProvider(String url);
+ }
+
+ DownloadFailedDialogInterface interfaceWithConfigurationWizard;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ try {
+ interfaceWithConfigurationWizard = (DownloadFailedDialogInterface) context;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(context.toString()
+ + " must implement NoticeDialogListener");
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ dialog.dismiss();
+ interfaceWithConfigurationWizard.cancelSettingUpProvider();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelable(KEY_PROVIDER, provider);
+ outState.putString(KEY_REASON_TO_FAIL, reasonToFail);
+ outState.putString(KEY_DOWNLOAD_ERROR, downloadError.toString());
+ }
+
+ private void restoreFromSavedInstance(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+ if (savedInstanceState.containsKey(KEY_PROVIDER)) {
+ this.provider = savedInstanceState.getParcelable(KEY_PROVIDER);
+ }
+ if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) {
+ this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL);
+ }
+ if (savedInstanceState.containsKey(KEY_DOWNLOAD_ERROR)) {
+ this.downloadError = valueOf(savedInstanceState.getString(KEY_DOWNLOAD_ERROR));
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java
new file mode 100644
index 00000000..5b5c94b4
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup;
+
+import android.os.Bundle;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+/**
+ * Created by cyberta on 17.08.18.
+ */
+
+public interface ProviderSetupInterface {
+ enum ProviderConfigState {
+ PROVIDER_NOT_SET,
+ SETTING_UP_PROVIDER,
+ SHOWING_PROVIDER_DETAILS,
+ PENDING_SHOW_PROVIDER_DETAILS,
+ PENDING_SHOW_FAILED_DIALOG,
+ SHOW_FAILED_DIALOG,
+ }
+
+ void handleProviderSetUp(Provider provider);
+ void handleProviderSetupFailed(Bundle resultData);
+ void handleCorrectlyDownloadedCertificate(Provider provider);
+ void handleIncorrectlyDownloadedCertificate();
+ Provider getProvider();
+ ProviderConfigState getConfigState();
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java
new file mode 100644
index 00000000..b7325e03
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java
@@ -0,0 +1,109 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+import butterknife.InjectView;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+
+public abstract class AbstractProviderDetailActivity extends ConfigWizardBaseActivity {
+
+ final public static String TAG = "providerDetailActivity";
+
+ @InjectView(R.id.provider_detail_description)
+ AppCompatTextView description;
+
+ @InjectView(R.id.provider_detail_options)
+ ListView options;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ provider = getIntent().getParcelableExtra(PROVIDER_KEY);
+ setContentView(R.layout.a_provider_detail);
+
+ if (provider == null) {
+ return;
+ }
+
+
+ setProviderHeaderText(provider.getName());
+ description.setText(provider.getDescription());
+
+ // Show only the options allowed by the provider
+ ArrayList<String> optionsList = new ArrayList<>();
+ if (provider.allowsRegistered()) {
+ optionsList.add(getString(R.string.login_to_profile));
+ optionsList.add(getString(R.string.create_profile));
+ if (provider.allowsAnonymous()) {
+ optionsList.add(getString(R.string.use_anonymously_button));
+ }
+ } else {
+ onAnonymouslySelected();
+ }
+
+
+ options.setAdapter(new ArrayAdapter<>(
+ this,
+ R.layout.v_single_list_item,
+ android.R.id.text1,
+ optionsList.toArray(new String[optionsList.size()])
+ ));
+ options.setOnItemClickListener((parent, view, position, id) -> {
+ String text = ((TextView) view).getText().toString();
+ Intent intent;
+ if (text.equals(getString(R.string.login_to_profile))) {
+ Log.d(TAG, "login selected");
+ intent = new Intent(getApplicationContext(), LoginActivity.class);
+ } else if (text.equals(getString(R.string.create_profile))) {
+ Log.d(TAG, "signup selected");
+ intent = new Intent(getApplicationContext(), SignupActivity.class);
+ } else {
+ onAnonymouslySelected();
+ return;
+ }
+ intent.putExtra(PROVIDER_KEY, provider);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP);
+ });
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ provider = intent.getParcelableExtra(PROVIDER_KEY);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
+ if (resultCode == RESULT_OK) {
+ setResult(resultCode, data);
+ finish();
+ }
+ }
+ }
+
+ private void onAnonymouslySelected() {
+ Intent intent;
+ Log.d(TAG, "use anonymously selected");
+ intent = new Intent();
+ intent.putExtra(Provider.KEY, provider);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java
new file mode 100644
index 00000000..0031f48d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java
@@ -0,0 +1,125 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.View;
+import android.widget.Button;
+
+import butterknife.InjectView;
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.providersetup.activities.ProviderListBaseActivity.EXTRAS_KEY_INVALID_URL;
+
+/**
+ * Created by cyberta on 30.06.18.
+ */
+
+public abstract class AddProviderBaseActivity extends ConfigWizardBaseActivity {
+
+ final public static String EXTRAS_KEY_NEW_URL = "NEW_URL";
+
+ @InjectView(R.id.text_uri_error)
+ TextInputLayout urlError;
+
+ @InjectView(R.id.text_uri)
+ TextInputEditText editUrl;
+
+ @InjectView(R.id.button_cancel)
+ Button cancelButton;
+
+ @InjectView(R.id.button_save)
+ Button saveButton;
+
+
+ protected void init() {
+ Bundle extras = this.getIntent().getExtras();
+ if (extras != null && extras.containsKey(EXTRAS_KEY_INVALID_URL)) {
+ editUrl.setText(extras.getString(EXTRAS_KEY_INVALID_URL));
+ saveButton.setEnabled(true);
+ }
+
+ setupSaveButton();
+ setupCancelButton();
+ setUpListeners();
+ setUpInitialUI();
+ }
+
+ public abstract void setupSaveButton();
+
+ private void setupCancelButton() {
+ cancelButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ finish();
+ }
+ });
+ }
+
+ private void setUpInitialUI() {
+ setProviderHeaderText(R.string.add_provider);
+ hideProgressBar();
+ }
+
+ protected void saveProvider() {
+ String entered_url = getURL();
+ if (validURL(entered_url)) {
+ Intent intent = this.getIntent();
+ intent.putExtra(EXTRAS_KEY_NEW_URL, entered_url);
+ setResult(RESULT_OK, intent);
+ finish();
+ } else {
+ editUrl.setText("");
+ urlError.setError(getString(R.string.not_valid_url_entered));
+ }
+ }
+
+ private void setUpListeners() {
+
+ editUrl.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (!validURL(getURL())) {
+ urlError.setError(getString(R.string.not_valid_url_entered));
+ saveButton.setEnabled(false);
+
+ } else {
+ urlError.setError(null);
+ saveButton.setEnabled(true);
+ }
+ }
+ });
+ }
+
+ private String getURL() {
+ String entered_url = editUrl.getText().toString().trim();
+ if (entered_url.contains("www.")) entered_url = entered_url.replaceFirst("www.", "");
+ if (!entered_url.startsWith("https://")) {
+ if (entered_url.startsWith("http://")) {
+ entered_url = entered_url.substring("http://".length());
+ }
+ entered_url = "https://".concat(entered_url);
+ }
+ return entered_url;
+ }
+
+ /**
+ * Checks if the entered url is valid or not.
+ *
+ * @param enteredUrl
+ * @return true if it's not empty nor contains only the protocol.
+ */
+ boolean validURL(String enteredUrl) {
+ return android.util.Patterns.WEB_URL.matcher(enteredUrl).matches();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java
new file mode 100644
index 00000000..22edd9ee
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2020 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.activities;
+
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+
+import butterknife.ButterKnife;
+
+/**
+ * Automatically inject with ButterKnife after calling setContentView
+ */
+
+public abstract class ButterKnifeActivity extends AppCompatActivity {
+
+ @Override
+ public void setContentView(View view) {
+ super.setContentView(view);
+ ButterKnife.inject(this);
+ }
+
+ @Override
+ public void setContentView(int layoutResID) {
+ super.setContentView(layoutResID);
+ ButterKnife.inject(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
new file mode 100644
index 00000000..3712c544
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java
@@ -0,0 +1,289 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.SharedPreferences;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.constraintlayout.widget.Guideline;
+import androidx.core.content.ContextCompat;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+
+import butterknife.InjectView;
+import butterknife.Optional;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.views.ProviderHeaderView;
+
+import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.SHARED_PREFERENCES;
+
+/**
+ * Base Activity for configuration wizard activities
+ *
+ * Created by fupduck on 09.01.18.
+ */
+
+public abstract class ConfigWizardBaseActivity extends ButterKnifeActivity {
+
+ private static final String TAG = ConfigWizardBaseActivity.class.getName();
+ public static final float GUIDE_LINE_COMPACT_DELTA = 0.1f;
+ protected SharedPreferences preferences;
+
+ @InjectView(R.id.header)
+ ProviderHeaderView providerHeaderView;
+
+ //Add provider screen has no loading screen
+ @Optional
+ @InjectView(R.id.loading_screen)
+ protected LinearLayout loadingScreen;
+
+ @Optional
+ @InjectView(R.id.progressbar)
+ protected ProgressBar progressBar;
+
+ @Optional
+ @InjectView(R.id.progressbar_description)
+ protected AppCompatTextView progressbarText;
+
+ //Only tablet layouts have guidelines as they are based on a ConstraintLayout
+ @Optional
+ @InjectView(R.id.guideline_top)
+ protected Guideline guideline_top;
+
+ @Optional
+ @InjectView(R.id.guideline_bottom)
+ protected Guideline guideline_bottom;
+
+ @InjectView(R.id.content)
+ protected LinearLayout content;
+
+ protected Provider provider;
+
+ protected boolean isCompactLayout = false;
+ protected boolean isActivityShowing;
+
+ private float defaultGuidelineTopPercentage;
+ private float defaultGuidelineBottomPercentage;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ preferences = getSharedPreferences(SHARED_PREFERENCES, MODE_PRIVATE);
+ provider = getIntent().getParcelableExtra(PROVIDER_KEY);
+ }
+
+ @Override
+ public void setContentView(View view) {
+ super.setContentView(view);
+ initContentView();
+ }
+
+ @Override
+ public void setContentView(int layoutResID) {
+ super.setContentView(layoutResID);
+ initContentView();
+ }
+
+ @Override
+ public void setContentView(View view, ViewGroup.LayoutParams params) {
+ super.setContentView(view, params);
+ initContentView();
+ }
+
+ private void initContentView() {
+ if (provider != null) {
+ setProviderHeaderText(provider.getName());
+ }
+ setProgressbarColorForPreLollipop();
+ setDefaultGuidelineValues();
+ setGlobalLayoutChangeListener();
+ }
+
+ private void setDefaultGuidelineValues() {
+ if (isTabletLayout()) {
+ defaultGuidelineTopPercentage = ((ConstraintLayout.LayoutParams) guideline_top.getLayoutParams()).guidePercent;
+ defaultGuidelineBottomPercentage = ((ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams()).guidePercent;
+ }
+ }
+
+ private void setProgressbarColorForPreLollipop() {
+ if (progressBar == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ return;
+ }
+ progressBar.getIndeterminateDrawable().setColorFilter(
+ ContextCompat.getColor(this, R.color.colorPrimary),
+ PorterDuff.Mode.SRC_IN);
+ }
+
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (provider != null) {
+ outState.putParcelable(PROVIDER_KEY, provider);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ isActivityShowing = false;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ isActivityShowing = true;
+ }
+
+ protected void restoreState(Bundle savedInstanceState) {
+ if (savedInstanceState != null && savedInstanceState.containsKey(PROVIDER_KEY)) {
+ provider = savedInstanceState.getParcelable(PROVIDER_KEY);
+ }
+ }
+
+ protected void setProviderHeaderLogo(@DrawableRes int providerHeaderLogo) {
+ providerHeaderView.setLogo(providerHeaderLogo);
+ }
+
+ protected void setProviderHeaderText(String providerHeaderText) {
+ providerHeaderView.setTitle(providerHeaderText);
+ }
+
+ protected void setProviderHeaderText(@StringRes int providerHeaderText) {
+ providerHeaderView.setTitle(providerHeaderText);
+ }
+
+ protected void hideProgressBar() {
+ if (loadingScreen == null) {
+ return;
+ }
+ loadingScreen.setVisibility(GONE);
+ content.setVisibility(VISIBLE);
+ }
+
+ protected void showProgressBar() {
+ if (loadingScreen == null) {
+ return;
+ }
+ content.setVisibility(GONE);
+ loadingScreen.setVisibility(VISIBLE);
+ }
+
+ protected void setProgressbarText(@StringRes int progressbarText) {
+ if (this.progressbarText == null) {
+ return;
+ }
+ this.progressbarText.setText(progressbarText);
+ }
+
+
+ protected void showCompactLayout() {
+ if (isCompactLayout) {
+ return;
+ }
+
+ if (isTabletLayoutInLandscape() || isPhoneLayout()) {
+ providerHeaderView.showCompactLayout();
+ }
+
+ showIncreasedTabletContentArea();
+ isCompactLayout = true;
+ }
+
+ protected void showStandardLayout() {
+ if (!isCompactLayout) {
+ return;
+ }
+ providerHeaderView.showStandardLayout();
+ showStandardTabletContentArea();
+ isCompactLayout = false;
+ }
+
+ private boolean isTabletLayoutInLandscape() {
+ // TabletLayout is based on a ConstraintLayout and uses Guidelines whereas the phone layout
+ // has no such elements in it's layout xml file
+ return guideline_top != null &&
+ guideline_bottom != null &&
+ getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE;
+ }
+
+ protected boolean isPhoneLayout() {
+ return guideline_top == null && guideline_bottom == null;
+ }
+
+ protected boolean isTabletLayout() {
+ return guideline_top != null && guideline_bottom != null;
+ }
+
+ /**
+ * Increases the white content area in tablet layouts
+ */
+ private void showIncreasedTabletContentArea() {
+ if (isPhoneLayout()) {
+ return;
+ }
+ ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams();
+ float increasedTopPercentage = defaultGuidelineTopPercentage - GUIDE_LINE_COMPACT_DELTA;
+ guideLineTopParams.guidePercent = increasedTopPercentage > 0f ? increasedTopPercentage : 0f;
+ guideline_top.setLayoutParams(guideLineTopParams);
+
+ ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams();
+ float increasedBottomPercentage = defaultGuidelineBottomPercentage + GUIDE_LINE_COMPACT_DELTA;
+ guideLineBottomParams.guidePercent = increasedBottomPercentage < 1f ? increasedBottomPercentage : 1f;
+ guideline_bottom.setLayoutParams(guideLineBottomParams);
+ }
+
+ /**
+ * Restores the default size of the white content area in tablet layouts
+ */
+ private void showStandardTabletContentArea() {
+ if (isPhoneLayout()) {
+ return;
+ }
+ ConstraintLayout.LayoutParams guideLineTopParams = (ConstraintLayout.LayoutParams) guideline_top.getLayoutParams();
+ guideLineTopParams.guidePercent = defaultGuidelineTopPercentage;
+ guideline_top.setLayoutParams(guideLineTopParams);
+
+ ConstraintLayout.LayoutParams guideLineBottomParams = (ConstraintLayout.LayoutParams) guideline_bottom.getLayoutParams();
+ guideLineBottomParams.guidePercent = defaultGuidelineBottomPercentage;
+ guideline_bottom.setLayoutParams(guideLineBottomParams);
+ }
+
+ /**
+ * Checks if the keyboard is shown and switches between the standard layout and the compact layout
+ */
+ private void setGlobalLayoutChangeListener() {
+ final View rootView = content.getRootView();
+ rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ Rect r = new Rect();
+ //r will be populated with the coordinates of your view that area still visible.
+ rootView.getWindowVisibleDisplayFrame(r);
+
+ float deltaHiddenScreen = 1f - ((float) (r.bottom - r.top) / (float) rootView.getHeight());
+ if (deltaHiddenScreen > 0.25f) {
+ // if more than 1/4 of the screen is hidden
+ showCompactLayout();
+ } else {
+ showStandardLayout();
+ }
+ }
+ });
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java
new file mode 100644
index 00000000..161c53d3
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.R;
+
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.preferAnonymousUsage;
+
+/**
+ * Created by cyberta on 17.08.18.
+ */
+
+public class CustomProviderSetupActivity extends ProviderSetupBaseActivity {
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setUpInitialUI();
+ restoreState(savedInstanceState);
+ setProvider(new Provider(BuildConfig.customProviderUrl, BuildConfig.geoipUrl, BuildConfig.customProviderIp, BuildConfig.customProviderApiIp));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (getConfigState() == ProviderConfigState.PROVIDER_NOT_SET) {
+ showProgressBar();
+ setupProvider();
+ }
+ }
+
+ private void setUpInitialUI() {
+ setContentView(R.layout.a_custom_provider_setup);
+ setProviderHeaderText(R.string.setup_provider);
+ hideProgressBar();
+ }
+
+ private void setupProvider() {
+ setProviderConfigState(SETTING_UP_PROVIDER);
+ ProviderAPICommand.execute(this, SET_UP_PROVIDER, getProvider());
+ }
+
+ // ------- ProviderSetupInterface ---v
+ @Override
+ public void handleProviderSetUp(Provider provider) {
+ setProvider(provider);
+ if (provider.allowsAnonymous()) {
+ downloadVpnCertificate();
+ } else {
+ showProviderDetails();
+ }
+ }
+
+ @Override
+ public void handleCorrectlyDownloadedCertificate(Provider provider) {
+ if (preferAnonymousUsage()) {
+ finishWithSetupWithProvider(provider);
+ } else {
+ this.provider = provider;
+ showProviderDetails();
+ }
+ }
+
+ // ------- DownloadFailedDialogInterface ---v
+ @Override
+ public void retrySetUpProvider(@NonNull Provider provider) {
+ setupProvider();
+ showProgressBar();
+ }
+
+ @Override
+ public void cancelSettingUpProvider() {
+ super.cancelSettingUpProvider();
+ finish();
+ }
+
+ @Override
+ public void addAndSelectNewProvider(String url) {
+ // ignore
+ }
+
+ private void finishWithSetupWithProvider(Provider provider) {
+ Intent intent = new Intent();
+ intent.putExtra(Provider.KEY, provider);
+ setResult(RESULT_OK, intent);
+ finish();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
+ setResult(resultCode, data);
+ finish();
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java
new file mode 100644
index 00000000..a8bac6d8
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java
@@ -0,0 +1,32 @@
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+
+import butterknife.OnClick;
+import se.leap.bitmaskclient.R;
+
+/**
+ * Activity to login to chosen Provider
+ *
+ * Created by fupduck on 09.01.18.
+ */
+
+public class LoginActivity extends ProviderCredentialsBaseActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setProgressbarText(R.string.logging_in);
+ setProviderHeaderLogo(R.drawable.logo);
+ setProviderHeaderText(R.string.login_to_profile);
+ }
+
+ @Override
+ @OnClick(R.id.button)
+ void handleButton() {
+ super.handleButton();
+ login(getUsername(), getPassword());
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java
new file mode 100644
index 00000000..91d0de56
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java
@@ -0,0 +1,479 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import com.google.android.material.textfield.TextInputEditText;
+import com.google.android.material.textfield.TextInputLayout;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.appcompat.widget.AppCompatTextView;
+import android.text.Editable;
+import android.text.Html;
+import android.text.TextWatcher;
+import android.text.method.LinkMovementMethod;
+import android.text.util.Linkify;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import butterknife.InjectView;
+import butterknife.OnClick;
+import se.leap.bitmaskclient.base.models.Constants.CREDENTIAL_ERRORS;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderAPI;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.R;
+
+import static android.text.TextUtils.isEmpty;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_CODE;
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_RESULT_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_PASSWORD;
+import static se.leap.bitmaskclient.base.models.Constants.CREDENTIALS_USERNAME;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.LOG_IN;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.USER_MESSAGE;
+
+/**
+ * Base Activity for activities concerning a provider interaction
+ *
+ * Created by fupduck on 09.01.18.
+ */
+
+public abstract class ProviderCredentialsBaseActivity extends ConfigWizardBaseActivity {
+
+ final protected static String TAG = ProviderCredentialsBaseActivity.class.getName();
+
+ final private static String ACTIVITY_STATE = "ACTIVITY STATE";
+
+ final private static String SHOWING_FORM = "SHOWING_FORM";
+ final private static String PERFORMING_ACTION = "PERFORMING_ACTION";
+ final private static String USERNAME_ERROR = "USERNAME_ERROR";
+ final private static String PASSWORD_ERROR = "PASSWORD_ERROR";
+ final private static String PASSWORD_VERIFICATION_ERROR = "PASSWORD_VERIFICATION_ERROR";
+
+ protected Intent mConfigState = new Intent(SHOWING_FORM);
+ protected ProviderAPIBroadcastReceiver providerAPIBroadcastReceiver;
+
+ @InjectView(R.id.provider_credentials_user_message)
+ AppCompatTextView userMessage;
+
+ @InjectView(R.id.provider_credentials_username)
+ TextInputEditText usernameField;
+
+ @InjectView(R.id.provider_credentials_password)
+ TextInputEditText passwordField;
+
+ @InjectView(R.id.provider_credentials_password_verification)
+ TextInputEditText passwordVerificationField;
+
+ @InjectView(R.id.provider_credentials_username_error)
+ TextInputLayout usernameError;
+
+ @InjectView(R.id.provider_credentials_password_error)
+ TextInputLayout passwordError;
+
+ @InjectView(R.id.provider_credentials_password_verification_error)
+ TextInputLayout passwordVerificationError;
+
+ @InjectView(R.id.button)
+ AppCompatButton button;
+
+ private boolean isUsernameError = false;
+ private boolean isPasswordError = false;
+ private boolean isVerificationError = false;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.a_provider_credentials);
+ providerAPIBroadcastReceiver = new ProviderAPIBroadcastReceiver();
+
+ IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT);
+ updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+ LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter);
+
+ setUpListeners();
+ restoreState(savedInstanceState);
+
+ String userMessageString = getIntent().getStringExtra(USER_MESSAGE);
+ if (userMessageString != null) {
+ userMessage.setText(userMessageString);
+ userMessage.setVisibility(VISIBLE);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ String action = mConfigState.getAction();
+ if (action == null) {
+ return;
+ }
+
+ if(action.equalsIgnoreCase(PERFORMING_ACTION)) {
+ showProgressBar();
+ }
+ }
+
+ protected void restoreState(Bundle savedInstance) {
+ super.restoreState(savedInstance);
+ if (savedInstance == null) {
+ return;
+ }
+ if (savedInstance.getString(USER_MESSAGE) != null) {
+ userMessage.setText(savedInstance.getString(USER_MESSAGE));
+ userMessage.setVisibility(VISIBLE);
+ }
+ updateUsernameError(savedInstance.getString(USERNAME_ERROR));
+ updatePasswordError(savedInstance.getString(PASSWORD_ERROR));
+ updateVerificationError(savedInstance.getString(PASSWORD_VERIFICATION_ERROR));
+ if (savedInstance.getString(ACTIVITY_STATE) != null) {
+ mConfigState.setAction(savedInstance.getString(ACTIVITY_STATE));
+ }
+ }
+
+ private void updateUsernameError(String usernameErrorString) {
+ usernameError.setError(usernameErrorString);
+ isUsernameError = usernameErrorString != null;
+ updateButton();
+ }
+
+ private void updatePasswordError(String passwordErrorString) {
+ passwordError.setError(passwordErrorString);
+ isPasswordError = passwordErrorString != null;
+ updateButton();
+ }
+
+ private void updateVerificationError(String verificationErrorString) {
+ passwordVerificationError.setError(verificationErrorString);
+ isVerificationError = verificationErrorString != null;
+ updateButton();
+ }
+
+ private void updateButton() {
+ button.setEnabled(!isPasswordError &&
+ !isUsernameError &&
+ !isVerificationError &&
+ !isEmpty(passwordField.getText()) &&
+ !isEmpty(usernameField.getText()) &&
+ !(passwordVerificationField.getVisibility() == VISIBLE &&
+ getPasswordVerification().length() == 0));
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putString(ACTIVITY_STATE, mConfigState.getAction());
+ if (userMessage.getText() != null && userMessage.getVisibility() == VISIBLE) {
+ outState.putString(USER_MESSAGE, userMessage.getText().toString());
+ }
+ if (usernameError.getError() != null) {
+ outState.putString(USERNAME_ERROR, usernameError.getError().toString());
+ }
+ if (passwordError.getError() != null) {
+ outState.putString(PASSWORD_ERROR, passwordError.getError().toString());
+ }
+ if (passwordVerificationError.getError() != null) {
+ outState.putString(PASSWORD_VERIFICATION_ERROR, passwordVerificationError.getError().toString());
+ }
+
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (providerAPIBroadcastReceiver != null)
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver);
+ }
+
+ @OnClick(R.id.button)
+ void handleButton() {
+ mConfigState.setAction(PERFORMING_ACTION);
+ hideKeyboard();
+ showProgressBar();
+ }
+
+ protected void setButtonText(@StringRes int buttonText) {
+ button.setText(buttonText);
+ }
+
+ String getUsername() {
+ String username = usernameField.getText().toString();
+ String providerDomain = provider.getDomain();
+ if (username.endsWith(providerDomain)) {
+ try {
+ return username.split("@" + providerDomain)[0];
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return "";
+ }
+ }
+ return username;
+ }
+
+ String getPassword() {
+ return passwordField.getText().toString();
+ }
+
+ String getPasswordVerification() {
+ return passwordVerificationField.getText().toString();
+ }
+
+ void login(String username, String password) {
+
+ Bundle parameters = bundleUsernameAndPassword(username, password);
+ ProviderAPICommand.execute(this, LOG_IN, parameters, provider);
+ }
+
+ public void signUp(String username, String password) {
+
+ Bundle parameters = bundleUsernameAndPassword(username, password);
+ ProviderAPICommand.execute(this, SIGN_UP, parameters, provider);
+ }
+
+ void downloadVpnCertificate(Provider handledProvider) {
+ provider = handledProvider;
+ ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider);
+ }
+
+ protected Bundle bundleUsernameAndPassword(String username, String password) {
+ Bundle parameters = new Bundle();
+ if (!username.isEmpty())
+ parameters.putString(CREDENTIALS_USERNAME, username);
+ if (!password.isEmpty())
+ parameters.putString(CREDENTIALS_PASSWORD, password);
+ return parameters;
+ }
+
+ private void setUpListeners() {
+ usernameField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if (getUsername().equalsIgnoreCase("")) {
+ s.clear();
+ updateUsernameError(getString(R.string.username_ask));
+ } else {
+ updateUsernameError(null);
+ String suffix = "@" + provider.getDomain();
+ if (!usernameField.getText().toString().endsWith(suffix)) {
+ s.append(suffix);
+ usernameField.setSelection(usernameField.getText().toString().indexOf('@'));
+ }
+ }
+ }
+ });
+ usernameField.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == IME_ACTION_DONE
+ || event != null && event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ passwordField.requestFocus();
+ return true;
+ }
+ return false;
+ }
+ });
+
+ passwordField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if(getPassword().length() < 8) {
+ updatePasswordError(getString(R.string.error_not_valid_password_user_message));
+ } else {
+ updatePasswordError(null);
+ }
+ }
+ });
+ passwordField.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == IME_ACTION_DONE
+ || event != null && event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ if (passwordVerificationField.getVisibility() == VISIBLE) {
+ passwordVerificationField.requestFocus();
+ } else {
+ button.performClick();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+
+ passwordVerificationField.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ if(getPassword().equals(getPasswordVerification())) {
+ updateVerificationError(null);
+ } else {
+ updateVerificationError(getString(R.string.password_mismatch));
+ }
+ }
+ });
+ passwordVerificationField.setOnEditorActionListener(new TextView.OnEditorActionListener() {
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == IME_ACTION_DONE
+ || event != null && event.getAction() == KeyEvent.ACTION_DOWN
+ && event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
+ button.performClick();
+ return true;
+ }
+ return false;
+ }
+ });
+ }
+
+ private void hideKeyboard() {
+ InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (imm != null) {
+ imm.hideSoftInputFromWindow(passwordField.getWindowToken(), 0);
+ }
+ }
+
+ private void handleReceivedErrors(Bundle arguments) {
+ if (arguments.containsKey(CREDENTIAL_ERRORS.PASSWORD_INVALID_LENGTH.toString())) {
+ updatePasswordError(getString(R.string.error_not_valid_password_user_message));
+ } else if (arguments.containsKey(CREDENTIAL_ERRORS.RISEUP_WARNING.toString())) {
+ userMessage.setVisibility(VISIBLE);
+ userMessage.setText(R.string.login_riseup_warning);
+ }
+ if (arguments.containsKey(CREDENTIALS_USERNAME)) {
+ String username = arguments.getString(CREDENTIALS_USERNAME);
+ usernameField.setText(username);
+ }
+ if (arguments.containsKey(CREDENTIAL_ERRORS.USERNAME_MISSING.toString())) {
+ updateUsernameError(getString(R.string.username_ask));
+ }
+ if (arguments.containsKey(USER_MESSAGE)) {
+ String userMessageString = arguments.getString(USER_MESSAGE);
+ try {
+ userMessageString = new JSONArray(userMessageString).getString(0);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
+ userMessage.setText(Html.fromHtml(userMessageString, Html.FROM_HTML_MODE_LEGACY));
+ } else {
+ userMessage.setText(Html.fromHtml(userMessageString));
+ }
+ Linkify.addLinks(userMessage, Linkify.ALL);
+ userMessage.setMovementMethod(LinkMovementMethod.getInstance());
+ userMessage.setVisibility(VISIBLE);
+ } else if (userMessage.getVisibility() != GONE) {
+ userMessage.setVisibility(GONE);
+ }
+
+ if (!usernameField.getText().toString().isEmpty() && passwordField.isFocusable())
+ passwordField.requestFocus();
+
+ mConfigState.setAction(SHOWING_FORM);
+ hideProgressBar();
+ }
+
+ private void successfullyFinished(Provider handledProvider) {
+ provider = handledProvider;
+ Intent resultData = new Intent();
+ resultData.putExtra(Provider.KEY, provider);
+ setResult(RESULT_OK, resultData);
+ finish();
+ }
+
+ //TODO: replace with EipSetupObserver
+ public class ProviderAPIBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "received Broadcast");
+
+ String action = intent.getAction();
+ if (action == null || !action.equalsIgnoreCase(BROADCAST_PROVIDER_API_EVENT)) {
+ return;
+ }
+
+ int resultCode = intent.getIntExtra(BROADCAST_RESULT_CODE, RESULT_CANCELED);
+ Bundle resultData = intent.getParcelableExtra(BROADCAST_RESULT_KEY);
+ Provider handledProvider = resultData.getParcelable(PROVIDER_KEY);
+
+ switch (resultCode) {
+ case ProviderAPI.SUCCESSFUL_SIGNUP:
+ String password = resultData.getString(CREDENTIALS_PASSWORD);
+ String username = resultData.getString(CREDENTIALS_USERNAME);
+ login(username, password);
+ break;
+ case ProviderAPI.SUCCESSFUL_LOGIN:
+ downloadVpnCertificate(handledProvider);
+ break;
+ case ProviderAPI.FAILED_LOGIN:
+ case ProviderAPI.FAILED_SIGNUP:
+ handleReceivedErrors((Bundle) intent.getParcelableExtra(BROADCAST_RESULT_KEY));
+ break;
+
+ case ProviderAPI.INCORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ // error handling takes place in MainActivity
+ case ProviderAPI.CORRECTLY_DOWNLOADED_VPN_CERTIFICATE:
+ successfullyFinished(handledProvider);
+ break;
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
new file mode 100644
index 00000000..46a40d11
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright (c) 2017 LEAP Encryption Access Project and contributors
+ * <p>
+ * 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.
+ * <p>
+ * 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.
+ * <p>
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.ListView;
+
+import androidx.annotation.NonNull;
+
+import com.pedrogomez.renderers.Renderer;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import butterknife.InjectView;
+import butterknife.OnItemClick;
+import se.leap.bitmaskclient.providersetup.AddProviderActivity;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderListActivity;
+import se.leap.bitmaskclient.providersetup.ProviderRenderer;
+import se.leap.bitmaskclient.providersetup.ProviderRendererBuilder;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.providersetup.ProviderListAdapter;
+
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_ADD_PROVIDER;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG;
+
+/**
+ * abstract base Activity that builds and shows the list of known available providers.
+ * The implementation of ProviderListBaseActivity differ in that they may or may not allow to bypass
+ * secure download mechanisms including certificate validation.
+ * <p/>
+ * It also allows the user to enter custom providers with a button.
+ *
+ * @author parmegv
+ * @author cyberta
+ */
+
+public abstract class ProviderListBaseActivity extends ProviderSetupBaseActivity {
+
+ @InjectView(R.id.provider_list)
+ protected ListView providerListView;
+ @Inject
+ protected ProviderListAdapter adapter;
+
+ final public static String TAG = ProviderListActivity.class.getSimpleName();
+ final protected static String EXTRAS_KEY_INVALID_URL = "INVALID_URL";
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setUpInitialUI();
+ initProviderList();
+ restoreState(savedInstanceState);
+ }
+
+ public abstract void retrySetUpProvider(@NonNull Provider provider);
+
+ protected abstract void onItemSelectedLogic();
+
+ private void initProviderList() {
+ List<Renderer<Provider>> prototypes = new ArrayList<>();
+ prototypes.add(new ProviderRenderer(this));
+ ProviderRendererBuilder providerRendererBuilder = new ProviderRendererBuilder(prototypes);
+ adapter = new ProviderListAdapter(getLayoutInflater(), providerRendererBuilder, getProviderManager());
+ providerListView.setAdapter(adapter);
+ }
+
+ private void setUpInitialUI() {
+ setContentView(R.layout.a_provider_list);
+ setProviderHeaderText(R.string.setup_provider);
+ hideProgressBar();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_CODE_CONFIGURE_LEAP) {
+ if (resultCode == RESULT_OK) {
+ setResult(resultCode, data);
+ finish();
+ }
+ } else if (requestCode == REQUEST_CODE_ADD_PROVIDER) {
+ if (resultCode == RESULT_OK) {
+ testNewURL = true;
+ String newUrl = data.getStringExtra(AddProviderActivity.EXTRAS_KEY_NEW_URL);
+ this.provider.setMainUrl(newUrl);
+ showAndSelectProvider(newUrl);
+ } else {
+ cancelSettingUpProvider();
+ }
+ }
+ }
+
+ public void showAndSelectProvider(String newURL) {
+ provider = new Provider(newURL, null, null);
+ autoSelectProvider();
+ }
+
+ private void autoSelectProvider() {
+ onItemSelectedLogic();
+ showProgressBar();
+ }
+
+ // ------- ProviderSetupInterface ---v
+ @Override
+ public void handleProviderSetUp(Provider handledProvider) {
+ this.provider = handledProvider;
+ adapter.add(provider);
+ adapter.saveProviders();
+ if (provider.allowsAnonymous()) {
+ //FIXME: providerApiBroadcastReceiver.getConfigState().putExtra(SERVICES_RETRIEVED, true); DEAD CODE???
+ downloadVpnCertificate();
+ } else {
+ showProviderDetails();
+ }
+ }
+
+ @Override
+ public void handleCorrectlyDownloadedCertificate(Provider handledProvider) {
+ this.provider = handledProvider;
+ showProviderDetails();
+ }
+
+ @OnItemClick(R.id.provider_list)
+ void onItemSelected(int position) {
+ if (SETTING_UP_PROVIDER == getConfigState() ||
+ SHOW_FAILED_DIALOG == getConfigState()) {
+ return;
+ }
+
+ //TODO Code 2 pane view
+ provider = adapter.getItem(position);
+ if (provider != null && !provider.isDefault()) {
+ //TODO Code 2 pane view
+ providerConfigState = SETTING_UP_PROVIDER;
+ showProgressBar();
+ onItemSelectedLogic();
+ } else {
+ addAndSelectNewProvider();
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (SETTING_UP_PROVIDER == providerConfigState ||
+ SHOW_FAILED_DIALOG == providerConfigState) {
+ cancelSettingUpProvider();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ /**
+ * Open the new provider dialog
+ */
+ public void addAndSelectNewProvider() {
+ Intent intent = new Intent(this, AddProviderActivity.class);
+ startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER);
+ }
+
+ /**
+ * Open the new provider dialog
+ */
+ @Override
+ public void addAndSelectNewProvider(String url) {
+ testNewURL = false;
+ Intent intent = new Intent(this, AddProviderActivity.class);
+ intent.putExtra(EXTRAS_KEY_INVALID_URL, url);
+ startActivityForResult(intent, REQUEST_CODE_ADD_PROVIDER);
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java
new file mode 100644
index 00000000..e54fb048
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentTransaction;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.jetbrains.annotations.NotNull;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import se.leap.bitmaskclient.base.FragmentManagerEnhanced;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.providersetup.ProviderAPICommand;
+import se.leap.bitmaskclient.providersetup.ProviderDetailActivity;
+import se.leap.bitmaskclient.providersetup.ProviderApiSetupBroadcastReceiver;
+import se.leap.bitmaskclient.providersetup.ProviderManager;
+import se.leap.bitmaskclient.providersetup.ProviderSetupFailedDialog;
+import se.leap.bitmaskclient.providersetup.ProviderSetupInterface;
+
+import static se.leap.bitmaskclient.base.models.Constants.BROADCAST_PROVIDER_API_EVENT;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_KEY;
+import static se.leap.bitmaskclient.base.models.Constants.REQUEST_CODE_CONFIGURE_LEAP;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.DOWNLOAD_VPN_CERTIFICATE;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.UPDATE_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_FAILED_DIALOG;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PENDING_SHOW_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.PROVIDER_NOT_SET;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SETTING_UP_PROVIDER;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOWING_PROVIDER_DETAILS;
+import static se.leap.bitmaskclient.providersetup.ProviderSetupInterface.ProviderConfigState.SHOW_FAILED_DIALOG;
+
+/**
+ * Created by cyberta on 19.08.18.
+ */
+
+public abstract class ProviderSetupBaseActivity extends ConfigWizardBaseActivity implements ProviderSetupInterface, ProviderSetupFailedDialog.DownloadFailedDialogInterface {
+ final public static String TAG = "PoviderSetupActivity";
+ final private static String ACTIVITY_STATE = "ACTIVITY STATE";
+ final private static String REASON_TO_FAIL = "REASON TO FAIL";
+
+ protected ProviderSetupInterface.ProviderConfigState providerConfigState = PROVIDER_NOT_SET;
+ private ProviderManager providerManager;
+ private FragmentManagerEnhanced fragmentManager;
+
+ private String reasonToFail;
+ protected boolean testNewURL;
+
+ private ProviderApiSetupBroadcastReceiver providerAPIBroadcastReceiver;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ fragmentManager = new FragmentManagerEnhanced(getSupportFragmentManager());
+ providerManager = ProviderManager.getInstance(getAssets(), getExternalFilesDir(null));
+ setUpProviderAPIResultReceiver();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "resuming with ConfigState: " + providerConfigState.toString());
+ if (SETTING_UP_PROVIDER == providerConfigState) {
+ showProgressBar();
+ } else if (PENDING_SHOW_FAILED_DIALOG == providerConfigState) {
+ showProgressBar();
+ showDownloadFailedDialog();
+ } else if (SHOW_FAILED_DIALOG == providerConfigState) {
+ showProgressBar();
+ } else if (SHOWING_PROVIDER_DETAILS == providerConfigState) {
+ cancelSettingUpProvider();
+ } else if (PENDING_SHOW_PROVIDER_DETAILS == providerConfigState) {
+ showProviderDetails();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (providerAPIBroadcastReceiver != null) {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(providerAPIBroadcastReceiver);
+ }
+ providerAPIBroadcastReceiver = null;
+ }
+
+
+ @Override
+ public void onSaveInstanceState(@NotNull Bundle outState) {
+ outState.putString(ACTIVITY_STATE, providerConfigState.toString());
+ outState.putString(REASON_TO_FAIL, reasonToFail);
+
+ super.onSaveInstanceState(outState);
+ }
+
+ protected FragmentManagerEnhanced getFragmentManagerEnhanced() {
+ return fragmentManager;
+ }
+
+ protected ProviderManager getProviderManager() {
+ return providerManager;
+ }
+
+ protected void setProviderConfigState(ProviderConfigState state) {
+ this.providerConfigState = state;
+ }
+
+ protected void setProvider(Provider provider) {
+ this.provider = provider;
+ }
+
+ // --------- ProviderSetupInterface ---v
+ @Override
+ public Provider getProvider() {
+ return provider;
+ }
+
+ @Override
+ public ProviderConfigState getConfigState() {
+ return providerConfigState;
+ }
+
+ @Override
+ public void handleProviderSetupFailed(Bundle resultData) {
+ reasonToFail = resultData.getString(ERRORS);
+ showDownloadFailedDialog();
+ }
+
+ @Override
+ public void handleIncorrectlyDownloadedCertificate() {
+ cancelSettingUpProvider();
+ setResult(RESULT_CANCELED, new Intent(getConfigState().toString()));
+ }
+
+ // -------- DownloadFailedDialogInterface ---v
+ @Override
+ public void cancelSettingUpProvider() {
+ providerConfigState = PROVIDER_NOT_SET;
+ provider = null;
+ hideProgressBar();
+ }
+
+ @Override
+ public void updateProviderDetails() {
+ providerConfigState = SETTING_UP_PROVIDER;
+ ProviderAPICommand.execute(this, UPDATE_PROVIDER_DETAILS, provider);
+ }
+
+ protected void restoreState(Bundle savedInstanceState) {
+ super.restoreState(savedInstanceState);
+ if (savedInstanceState == null) {
+ return;
+ }
+ this.providerConfigState = ProviderSetupInterface.ProviderConfigState.valueOf(savedInstanceState.getString(ACTIVITY_STATE, PROVIDER_NOT_SET.toString()));
+ if (savedInstanceState.containsKey(REASON_TO_FAIL)) {
+ reasonToFail = savedInstanceState.getString(REASON_TO_FAIL);
+ }
+ }
+
+ private void setUpProviderAPIResultReceiver() {
+ providerAPIBroadcastReceiver = new ProviderApiSetupBroadcastReceiver(this);
+
+ IntentFilter updateIntentFilter = new IntentFilter(BROADCAST_PROVIDER_API_EVENT);
+ updateIntentFilter.addCategory(Intent.CATEGORY_DEFAULT);
+ LocalBroadcastManager.getInstance(this).registerReceiver(providerAPIBroadcastReceiver, updateIntentFilter);
+ }
+
+ /**
+ * Asks ProviderApiService to download an anonymous (anon) VPN certificate.
+ */
+ protected void downloadVpnCertificate() {
+ ProviderAPICommand.execute(this, DOWNLOAD_VPN_CERTIFICATE, provider);
+ }
+
+ /**
+ * Once selected a provider, this fragment offers the user to log in,
+ * use it anonymously (if possible)
+ * or cancel his/her election pressing the back button.
+ */
+ public void showProviderDetails() {
+ // show only if current activity is shown
+ if (isActivityShowing &&
+ providerConfigState != SHOWING_PROVIDER_DETAILS) {
+ providerConfigState = SHOWING_PROVIDER_DETAILS;
+ Intent intent = new Intent(this, ProviderDetailActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
+ intent.putExtra(PROVIDER_KEY, provider);
+ startActivityForResult(intent, REQUEST_CODE_CONFIGURE_LEAP);
+ } else {
+ providerConfigState = PENDING_SHOW_PROVIDER_DETAILS;
+ }
+ }
+
+ /**
+ * Shows an error dialog, if configuring of a provider failed.
+ */
+ public void showDownloadFailedDialog() {
+ try {
+ providerConfigState = SHOW_FAILED_DIALOG;
+ FragmentTransaction fragmentTransaction = fragmentManager.removePreviousFragment(ProviderSetupFailedDialog.TAG);
+ DialogFragment newFragment;
+ try {
+ JSONObject errorJson = new JSONObject(reasonToFail);
+ newFragment = ProviderSetupFailedDialog.newInstance(provider, errorJson, testNewURL);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ newFragment = ProviderSetupFailedDialog.newInstance(provider, reasonToFail);
+ } catch (NullPointerException e) {
+ //reasonToFail was null
+ return;
+ }
+ newFragment.show(fragmentTransaction, ProviderSetupFailedDialog.TAG);
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ providerConfigState = PENDING_SHOW_FAILED_DIALOG;
+ }
+ }
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java
new file mode 100644
index 00000000..c0245845
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.activities;
+
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+
+import butterknife.OnClick;
+import se.leap.bitmaskclient.R;
+
+import static android.view.View.VISIBLE;
+
+/**
+ * Create an account with a provider
+ */
+
+public class SignupActivity extends ProviderCredentialsBaseActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setProviderHeaderLogo(R.drawable.logo);
+ setProviderHeaderText(R.string.create_profile);
+
+ setProgressbarText(R.string.signing_up);
+ setButtonText(R.string.signup_button);
+
+ passwordVerificationField.setVisibility(VISIBLE);
+ passwordVerificationError.setVisibility(VISIBLE);
+ }
+
+ @Override
+ @OnClick(R.id.button)
+ void handleButton() {
+ super.handleButton();
+ if (getPassword().equals(getPasswordVerification())) {
+ signUp(getUsername(), getPassword());
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java
new file mode 100644
index 00000000..44de1e6d
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java
@@ -0,0 +1,39 @@
+package se.leap.bitmaskclient.providersetup.connectivity;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+
+import okhttp3.Dns;
+import se.leap.bitmaskclient.base.models.Provider;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.base.utils.IPAddress;
+
+class DnsResolver implements Dns {
+
+ @Override
+ public List<InetAddress> lookup(@NotNull String hostname) throws UnknownHostException {
+ try {
+ return Dns.SYSTEM.lookup(hostname);
+ } catch (UnknownHostException e) {
+ ProviderObservable observable = ProviderObservable.getInstance();
+ Provider currentProvider;
+ if (observable.getProviderForDns() != null) {
+ currentProvider = observable.getProviderForDns();
+ } else {
+ currentProvider = observable.getCurrentProvider();
+ }
+ String ip = currentProvider.getIpForHostname(hostname);
+ if (!ip.isEmpty()) {
+ ArrayList<InetAddress> addresses = new ArrayList<>();
+ addresses.add(InetAddress.getByAddress(hostname, IPAddress.asBytes(ip)));
+ return addresses;
+ } else {
+ throw new UnknownHostException("Hostname " + hostname + " not found");
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
new file mode 100644
index 00000000..2077a8b9
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.providersetup.connectivity;
+
+import android.content.res.Resources;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+import okhttp3.CipherSuite;
+import okhttp3.ConnectionSpec;
+import okhttp3.Cookie;
+import okhttp3.CookieJar;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+import okhttp3.TlsVersion;
+
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.R.string.certificate_error;
+import static se.leap.bitmaskclient.R.string.error_io_exception_user_message;
+import static se.leap.bitmaskclient.R.string.error_no_such_algorithm_exception_user_message;
+import static se.leap.bitmaskclient.R.string.keyChainAccessError;
+import static se.leap.bitmaskclient.R.string.server_unreachable_message;
+import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.getProviderFormattedString;
+
+/**
+ * Created by cyberta on 08.01.18.
+ */
+
+public class OkHttpClientGenerator {
+
+ Resources resources;
+
+ public OkHttpClientGenerator(/*SharedPreferences preferences,*/ Resources resources) {
+ this.resources = resources;
+ }
+
+ public OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
+ return initHttpClient(initError, null);
+ }
+
+ public OkHttpClient initSelfSignedCAHttpClient(String caCert, JSONObject initError) {
+ return initHttpClient(initError, caCert);
+ }
+
+ public OkHttpClient init() {
+ try {
+ return createClient(null);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
+ if (resources == null) {
+ return null;
+ }
+ try {
+ return createClient(certificate);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ // TODO ca cert is invalid - show better error ?!
+ addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error));
+ } catch (IllegalStateException | KeyManagementException | KeyStoreException e) {
+ e.printStackTrace();
+ addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
+ e.printStackTrace();
+ addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message));
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ // TODO ca cert is invalid - show better error ?!
+ addErrorMessageToJson(initError, getProviderFormattedString(resources, certificate_error));
+ } catch (UnknownHostException e) {
+ e.printStackTrace();
+ addErrorMessageToJson(initError, resources.getString(server_unreachable_message));
+ } catch (IOException e) {
+ e.printStackTrace();
+ addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message));
+ } catch (Exception e) {
+ e.printStackTrace();
+ // unexpected exception, should never happen
+ // only to shorten the method signature createClient(String certificate)
+ }
+ return null;
+ }
+
+ private OkHttpClient createClient(String certificate) throws Exception {
+ TLSCompatSocketFactory sslCompatFactory;
+ ConnectionSpec spec = getConnectionSpec();
+ OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
+
+ if (!isEmpty(certificate)) {
+ sslCompatFactory = new TLSCompatSocketFactory(certificate);
+ } else {
+ sslCompatFactory = new TLSCompatSocketFactory();
+ }
+ sslCompatFactory.initSSLSocketFactory(clientBuilder);
+ clientBuilder.cookieJar(getCookieJar())
+ .connectionSpecs(Collections.singletonList(spec));
+ clientBuilder.dns(new DnsResolver());
+ return clientBuilder.build();
+ }
+
+
+
+ @NonNull
+ private ConnectionSpec getConnectionSpec() {
+ ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
+ //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels
+ //figure out how to use bcjsse for that purpose
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
+ connectionSpecbuilder.cipherSuites(
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+ );
+ return connectionSpecbuilder.build();
+ }
+
+ @NonNull
+ private CookieJar getCookieJar() {
+ return new CookieJar() {
+ private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
+
+ @Override
+ public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
+ cookieStore.put(url.host(), cookies);
+ }
+
+ @Override
+ public List<Cookie> loadForRequest(HttpUrl url) {
+ List<Cookie> cookies = cookieStore.get(url.host());
+ return cookies != null ? cookies : new ArrayList<Cookie>();
+ }
+ };
+ }
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java
new file mode 100644
index 00000000..5357fd74
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java
@@ -0,0 +1,158 @@
+package se.leap.bitmaskclient.providersetup.connectivity;
+
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyManagementException;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.base.utils.ConfigHelper;
+
+/**
+ * Created by cyberta on 24.10.17.
+ * This class ensures that modern TLS algorithms will also be used on old devices (Android 4.1 - Android 4.4.4) in order to avoid
+ * attacks like POODLE.
+ */
+
+public class TLSCompatSocketFactory extends SSLSocketFactory {
+
+ private static final String TAG = TLSCompatSocketFactory.class.getName();
+ private SSLSocketFactory internalSSLSocketFactory;
+ private TrustManager trustManager;
+
+ public TLSCompatSocketFactory(String trustedCaCert) throws KeyManagementException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, NoSuchProviderException {
+ initForSelfSignedCAs(trustedCaCert);
+ }
+
+ public TLSCompatSocketFactory() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException, IOException {
+ initForCommercialCAs();
+ }
+
+ public void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException {
+ builder.sslSocketFactory(this, (X509TrustManager) trustManager);
+ }
+
+
+ private void initForSelfSignedCAs(String trustedSelfSignedCaCert) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, IllegalStateException, KeyManagementException, NoSuchProviderException {
+ // Create a KeyStore containing our trusted CAs
+ String defaultType = KeyStore.getDefaultType();
+ KeyStore keyStore = KeyStore.getInstance(defaultType);
+ keyStore.load(null, null);
+ if (!TextUtils.isEmpty(trustedSelfSignedCaCert)) {
+ java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(trustedSelfSignedCaCert);
+ keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate);
+ }
+
+ // Create a TrustManager that trusts the CAs in our KeyStore
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init(keyStore);
+
+ // Check if there's only 1 X509Trustmanager -> from okttp3 source code example
+ TrustManager[] trustManagers = tmf.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+
+ trustManager = trustManagers[0];
+
+ // Create a SSLContext that uses our TrustManager
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(null, tmf.getTrustManagers(), null);
+ internalSSLSocketFactory = sslContext.getSocketFactory();
+
+ }
+
+
+ private void initForCommercialCAs() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
+
+ // Create a TrustManager that trusts the CAs in our KeyStore
+ String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
+ TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
+ tmf.init((KeyStore) null);
+
+ // Check if there's only 1 X509Trustmanager -> from okttp3 source code example
+ TrustManager[] trustManagers = tmf.getTrustManagers();
+ if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
+ throw new IllegalStateException("Unexpected default trust managers:"
+ + Arrays.toString(trustManagers));
+ }
+
+ trustManager = trustManagers[0];
+
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, null, null);
+ internalSSLSocketFactory = context.getSocketFactory();
+ }
+
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return internalSSLSocketFactory.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return internalSSLSocketFactory.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket());
+ }
+
+ @Override
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException, IllegalArgumentException {
+ return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
+ }
+
+ private Socket enableTLSOnSocket(Socket socket) throws IllegalArgumentException {
+ if(socket != null && (socket instanceof SSLSocket)) {
+ ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
+ //TODO: add a android version check as soon as a new Android API or bcjsse supports TLSv1.3
+ }
+ return socket;
+
+
+ }
+
+
+
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java
new file mode 100644
index 00000000..8e9d3e32
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java
@@ -0,0 +1,361 @@
+/**
+ * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient.providersetup.models;
+
+
+import org.jboss.security.srp.SRPParameters;
+
+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 se.leap.bitmaskclient.base.utils.ConfigHelper;
+
+/**
+ * Implements all SRP algorithm logic.
+ * <p/>
+ * 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";
+ final public static String TAG = "Leap SRP session class tag";
+
+ 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
+ */
+ public LeapSRPSession(String username, String password) {
+ this(username, password, 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 abytes, the random exponent used in the A public key
+ */
+ public LeapSRPSession(String username, String password, byte[] abytes) {
+
+ params = new SRPParameters(new BigInteger(ConfigHelper.NG_1024, 16).toByteArray(), ConfigHelper.G.toByteArray(), BigInteger.ZERO.toByteArray(), "SHA-256");
+ 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;
+ }
+
+ public byte[] calculateNewSalt() {
+ try {
+ BigInteger salt = new BigInteger(64, SecureRandom.getInstance("SHA1PRNG"));
+ return ConfigHelper.trim(salt.toByteArray());
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * Calculates the parameter V of the SRP-6a algorithm.
+ *
+ * @return the value of V
+ */
+ public BigInteger calculateV(String username, String password, byte[] salt) {
+ byte[] x_bytes = calculatePasswordHash(username, password, ConfigHelper.trim(salt));
+ x = new BigInteger(1, x_bytes);
+ BigInteger v = 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
+ * @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) {
+ // Calculate x = H(s | H(U | ':' | password))
+ byte[] M1 = null;
+ if (new BigInteger(1, Bbytes).mod(new BigInteger(1, N_bytes)) != BigInteger.ZERO) {
+ this.v = calculateV(username, password, salt_bytes);
+ // 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)
+ MessageDigest sessionDigest = newDigest();
+ 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);
+ String k_string = "bf66c44a428916cad64aa7c679f3fd897ad4c375e9bbb4cbf2f5de241d618ef0";
+ BigInteger k = new BigInteger(k_string, 16);
+ BigInteger B_minus_v = B.subtract(k.multiply(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;
+ }
+
+ public static void setToken(String token) {
+ LeapSRPSession.token = token;
+ }
+
+ public static String getToken() {
+ return token;
+ }
+
+ public static boolean loggedIn() {
+ return !token.isEmpty();
+ }
+
+ /**
+ * @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;
+ }
+
+ public byte[] getK() {
+ return K;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java
new file mode 100644
index 00000000..46a62626
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java
@@ -0,0 +1,26 @@
+package se.leap.bitmaskclient.providersetup.models;
+
+import com.google.gson.Gson;
+
+/**
+ * Created by cyberta on 23.10.17.
+ */
+
+public class SrpCredentials {
+
+ /**
+ * Parameter A of SRP authentication
+ */
+ private String A;
+ private String login;
+
+ public SrpCredentials(String username, String A) {
+ this.login = username;
+ this.A = A;
+ }
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java
new file mode 100644
index 00000000..31228edf
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java
@@ -0,0 +1,42 @@
+package se.leap.bitmaskclient.providersetup.models;
+
+import com.google.gson.Gson;
+
+/**
+ * Created by cyberta on 23.10.17.
+ */
+
+public class SrpRegistrationData {
+
+
+ private User user;
+
+ public SrpRegistrationData(String username, String passwordSalt, String passwordVerifier) {
+ user = new User(username, passwordSalt, passwordVerifier);
+ }
+
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+
+
+ public class User {
+
+ String login;
+ String password_salt;
+ String password_verifier;
+
+ public User(String login, String password_salt, String password_verifier) {
+ this.login = login;
+ this.password_salt = password_salt;
+ this.password_verifier = password_verifier;
+ }
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+ }
+}