From 6b032b751324a30120cfaabe88940f95171df11f Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 29 Dec 2020 00:54:08 +0100 Subject: new year cleanup: restructure messy project --- .../bitmaskclient/providersetup/ProviderAPI.java | 128 +++ .../providersetup/ProviderAPICommand.java | 86 ++ .../providersetup/ProviderApiConnector.java | 98 +++ .../providersetup/ProviderApiManagerBase.java | 946 +++++++++++++++++++++ .../ProviderApiSetupBroadcastReceiver.java | 84 ++ .../providersetup/ProviderListAdapter.java | 21 + .../providersetup/ProviderManager.java | 272 ++++++ .../providersetup/ProviderRenderer.java | 57 ++ .../providersetup/ProviderRendererBuilder.java | 21 + .../providersetup/ProviderSetupFailedDialog.java | 189 ++++ .../providersetup/ProviderSetupInterface.java | 43 + .../activities/AbstractProviderDetailActivity.java | 109 +++ .../activities/AddProviderBaseActivity.java | 125 +++ .../activities/ButterKnifeActivity.java | 46 + .../activities/ConfigWizardBaseActivity.java | 289 +++++++ .../activities/CustomProviderSetupActivity.java | 121 +++ .../providersetup/activities/LoginActivity.java | 32 + .../ProviderCredentialsBaseActivity.java | 479 +++++++++++ .../activities/ProviderListBaseActivity.java | 193 +++++ .../activities/ProviderSetupBaseActivity.java | 240 ++++++ .../providersetup/activities/SignupActivity.java | 55 ++ .../providersetup/connectivity/DnsResolver.java | 39 + .../connectivity/OkHttpClientGenerator.java | 182 ++++ .../connectivity/TLSCompatSocketFactory.java | 158 ++++ .../providersetup/models/LeapSRPSession.java | 361 ++++++++ .../providersetup/models/SrpCredentials.java | 26 + .../providersetup/models/SrpRegistrationData.java | 42 + 27 files changed, 4442 insertions(+) create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPI.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderAPICommand.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiConnector.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiManagerBase.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderApiSetupBroadcastReceiver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderListAdapter.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderManager.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRenderer.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderRendererBuilder.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupFailedDialog.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/ProviderSetupInterface.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AbstractProviderDetailActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/AddProviderBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ButterKnifeActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ConfigWizardBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/CustomProviderSetupActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/LoginActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderCredentialsBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderListBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/ProviderSetupBaseActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/activities/SignupActivity.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/DnsResolver.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/OkHttpClientGenerator.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/connectivity/TLSCompatSocketFactory.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/LeapSRPSession.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpCredentials.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/providersetup/models/SrpRegistrationData.java (limited to 'app/src/main/java/se/leap/bitmaskclient/providersetup') 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 . + */ +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. + *

+ * 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 . + */ + +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> 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 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 . + */ + +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>(), okHttpClient); + } + + protected String sendGetStringToServer(@NonNull String url, @NonNull List> 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> 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> 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> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair 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 . + */ +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 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 { + public ProviderListAdapter(LayoutInflater layoutInflater, RendererBuilder rendererBuilder, + AdapteeCollection 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 { + + private AssetManager assetsManager; + private File externalFilesDir; + private Set defaultProviders; + private Set customProviders; + private Set defaultProviderURLs; + private Set 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 getProviderUrlSetFromProviderSet(Set providers) { + HashSet providerUrls = new HashSet<>(); + for (Provider provider : providers) { + providerUrls.add(provider.getMainUrl().getUrl()); + } + return providerUrls; + } + + private Set providersFromAssets(String directory, String[] relativeFilePaths) { + Set 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 providersFromFiles(String[] files) { + Set 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 providers() { + List 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 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 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 persistedCustomProviders = externalFilesDir != null && externalFilesDir.isDirectory() ? + providersFromFiles(externalFilesDir.list()) : new HashSet(); + 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 { + 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 { + public ProviderRendererBuilder(Collection> 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 . + */ +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 . + */ +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 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 . + */ +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 . + */ +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 . + */ +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 + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package se.leap.bitmaskclient.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. + *

+ * 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> 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 . + */ +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 . + */ +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 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 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 . + */ + +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> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + 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 . + */ +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. + *

+ * 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); + } + } +} -- cgit v1.2.3