summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcyBerta <cyberta@riseup.net>2018-01-09 20:55:10 +0100
committercyBerta <cyberta@riseup.net>2018-01-09 20:55:10 +0100
commit0b647ea5e7ff67747080b2ffcebc948da0fbecb5 (patch)
treee01b59b626aded786cca8d25f7749c52b9150194
parent81a732702f7b3125ac543f92d8a5ec33cce972fe (diff)
8773 refactoring ProviderAPI for testability, setting up basic unit test framework
-rw-r--r--app/build.gradle14
-rw-r--r--app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java (renamed from app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java)64
-rw-r--r--app/src/main/AndroidManifest.xml2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java60
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java16
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java124
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java4
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java102
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java (renamed from app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java)343
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java2
-rw-r--r--app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java (renamed from app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java)81
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/TestUtils.java26
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java10
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java249
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java10
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java165
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java443
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java82
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java58
-rw-r--r--app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java192
-rw-r--r--app/src/test/resources/error_messages.json16
-rw-r--r--app/src/test/resources/riseup.net.json37
-rw-r--r--app/src/test/resources/riseup.net.pem32
-rw-r--r--app/src/test/resources/riseup.service.json93
-rw-r--r--app/src/test/resources/secrets.json2
27 files changed, 2051 insertions, 346 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 048eb597..8b956944 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -64,7 +64,16 @@ android {
}
dependencies {
- testCompile 'org.mockito:mockito-core:2.6.3'
+ testCompile 'junit:junit:4.12'
+ testCompile 'org.mockito:mockito-core:2.8.0'
+ testCompile 'org.powermock:powermock-api-mockito2:1.7.3'
+ testCompile 'org.powermock:powermock-module-junit4:1.7.3'
+ testCompile 'org.powermock:powermock-core:1.7.3'
+ testCompile 'org.powermock:powermock-module-junit4-rule:1.7.3'
+ testCompile 'com.madgag.spongycastle:bctls-jdk15on:1.58.0.0'
+ testCompile 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0'
+ testCompile 'com.madgag.spongycastle:bcpg-jdk15on:1.58.0.0'
+
androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3'
testCompile 'junit:junit:4.12'
testCompile 'org.json:json:20170516'
@@ -72,8 +81,7 @@ dependencies {
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
compile 'com.github.pedrovgs:renderers:1.5'
compile 'com.intellij:annotations:12.0'
- compile 'com.google.code.gson:gson:2.4'
- compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0'
+ compile 'com.google.code.gson:gson:2.7'
compile 'com.squareup.okhttp3:okhttp:3.9.0'
compile 'mbanje.kurt:fabbutton:1.1.4'
compile "com.android.support:support-core-utils:26.1.0"
diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
index 5cb06115..9c27fd1f 100644
--- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ * 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
@@ -14,8 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
package se.leap.bitmaskclient;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.os.Bundle;
import android.util.Pair;
@@ -42,26 +45,27 @@ import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
-import se.leap.bitmaskclient.ProviderListContent.ProviderItem;
import se.leap.bitmaskclient.eip.EIP;
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
import static se.leap.bitmaskclient.R.string.certificate_error;
import static se.leap.bitmaskclient.R.string.malformed_url;
/**
- * Implements HTTP api methods used to manage communications with the provider server.
- * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors
- * of ProviderAPI.
- * <p/>
- * It extends an IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Created by cyberta on 04.01.18.
*/
-public class ProviderAPI extends ProviderApiBase {
- private static boolean lastDangerOn = true;
+public class ProviderApiManager extends ProviderApiManagerBase {
+ protected static boolean lastDangerOn = true;
+
+
+ public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ super(preferences, resources, clientGenerator, callback);
+ }
public static boolean lastDangerOn() {
return lastDangerOn;
@@ -79,10 +83,17 @@ public class ProviderAPI extends ProviderApiBase {
Bundle currentDownload = new Bundle();
if (task != null) {
- lastDangerOn = task.containsKey(ProviderItem.DANGER_ON) && task.getBoolean(ProviderItem.DANGER_ON);
+ lastDangerOn = task.containsKey(ProviderListContent.ProviderItem.DANGER_ON) && task.getBoolean(ProviderListContent.ProviderItem.DANGER_ON);
lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ?
task.getString(Provider.MAIN_URL) :
"";
+
+ if (isEmpty(lastProviderMainUrl)) {
+ currentDownload.putBoolean(RESULT_KEY, false);
+ setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null);
+ return currentDownload;
+ }
+
providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
task.getString(Provider.CA_CERT_FINGERPRINT) :
"";
@@ -150,16 +161,16 @@ public class ProviderAPI extends ProviderApiBase {
providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition, dangerOn);
if (!isValidJson(providerDotJsonString)) {
- result.putString(ERRORS, getString(malformed_url));
+ result.putString(ERRORS, resources.getString(malformed_url));
result.putBoolean(RESULT_KEY, false);
return result;
}
try {
JSONObject providerJson = new JSONObject(providerDotJsonString);
- String providerDomain = providerJson.getString(Provider.DOMAIN);
+ String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
providerApiUrl = getApiUrlWithVersion(providerJson);
- String name = providerJson.getString(Provider.NAME);
+ //String name = providerJson.getString(Provider.NAME);
//TODO setProviderName(name);
preferences.edit().putString(Provider.KEY, providerJson.toString()).
@@ -249,13 +260,11 @@ public class ProviderAPI extends ProviderApiBase {
preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, certString).commit();
result.putBoolean(RESULT_KEY, true);
} else {
- String reason_to_fail = pickErrorMessage(certString);
- result.putString(ERRORS, reason_to_fail);
+ setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString());
result.putBoolean(RESULT_KEY, false);
}
} catch (JSONException e) {
- String reason_to_fail = formatErrorMessage(malformed_url);
- result.putString(ERRORS, reason_to_fail);
+ setErrorResult(result, resources.getString(malformed_url), null);
result.putBoolean(RESULT_KEY, false);
}
@@ -275,7 +284,7 @@ public class ProviderAPI extends ProviderApiBase {
String responseString;
JSONObject errorJson = new JSONObject();
- OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson);
+ OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson);
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -288,7 +297,7 @@ public class ProviderAPI extends ProviderApiBase {
try {
// try to download with provider CA on certificate error
JSONObject responseErrorJson = new JSONObject(responseString);
- if (danger_on && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+ if (danger_on && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
responseString = downloadWithoutCA(string_url);
}
} catch (JSONException e) {
@@ -303,7 +312,7 @@ public class ProviderAPI extends ProviderApiBase {
String responseString;
JSONObject errorJson = new JSONObject();
String baseUrl = getApiUrl(providerDefinition);
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -316,7 +325,7 @@ public class ProviderAPI extends ProviderApiBase {
try {
// try to download with provider CA on certificate error
JSONObject responseErrorJson = new JSONObject(responseString);
- if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+ if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
responseString = downloadWithCommercialCA(urlString, dangerOn);
}
} catch (JSONException e) {
@@ -338,7 +347,7 @@ public class ProviderAPI extends ProviderApiBase {
JSONObject initError = new JSONObject();
String responseString;
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError);
if (okHttpClient == null) {
return initError.toString();
}
@@ -351,7 +360,7 @@ public class ProviderAPI extends ProviderApiBase {
try {
// danger danger: try to download without CA on certificate error
JSONObject responseErrorJson = new JSONObject(responseString);
- if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+ if (dangerOn && responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
responseString = downloadWithoutCA(urlString);
}
} catch (JSONException e) {
@@ -418,5 +427,4 @@ public class ProviderAPI extends ProviderApiBase {
}
return string;
}
-
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dd11143..c0e2d90c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,7 +54,7 @@
</intent-filter>
</service>
<service android:name="se.leap.bitmaskclient.ProviderAPI" android:enabled="true"/>
-
+
<receiver
android:name="se.leap.bitmaskclient.OnBootReceiver"
android:enabled="true"
diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
index 2c169e3d..c26184bb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
@@ -56,7 +56,7 @@ import se.leap.bitmaskclient.userstatus.SessionDialog;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
-import static se.leap.bitmaskclient.ProviderApiBase.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
/**
* abstract base Activity that builds and shows the list of known available providers.
@@ -352,7 +352,7 @@ public abstract class BaseConfigurationWizard extends Activity
}
/**
- * Asks ProviderAPI to download an anonymous (anon) VPN certificate.
+ * Asks ProviderApiService to download an anonymous (anon) VPN certificate.
*/
private void downloadVpnCertificate() {
Intent provider_API_command = new Intent(this, ProviderAPI.class);
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index ed527a54..54bcc1f4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -16,16 +16,33 @@
*/
package se.leap.bitmaskclient;
-import android.util.*;
-
-import org.json.*;
-
-import java.io.*;
-import java.math.*;
-import java.security.*;
-import java.security.cert.*;
-import java.security.interfaces.*;
-import java.security.spec.*;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Base64;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
import static android.R.attr.name;
@@ -82,7 +99,7 @@ public class ConfigHelper {
cf = CertificateFactory.getInstance("X.509");
certificate_string = certificate_string.replaceFirst("-----BEGIN CERTIFICATE-----", "").replaceFirst("-----END CERTIFICATE-----", "").trim();
- byte[] cert_bytes = Base64.decode(certificate_string, Base64.DEFAULT);
+ byte[] cert_bytes = Base64.decode(certificate_string);
InputStream caInput = new ByteArrayInputStream(cert_bytes);
try {
certificate = cf.generateCertificate(caInput);
@@ -90,15 +107,9 @@ public class ConfigHelper {
} finally {
caInput.close();
}
- } catch (CertificateException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- return null;
- } catch (IllegalArgumentException e) {
+ } catch (NullPointerException | CertificateException | IOException | IllegalArgumentException e) {
return null;
}
-
return (X509Certificate) certificate;
}
@@ -139,7 +150,7 @@ public class ConfigHelper {
try {
KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString, Base64.DEFAULT));
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
key = (RSAPrivateKey) kf.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
// TODO Auto-generated catch block
@@ -162,7 +173,7 @@ public class ConfigHelper {
}
public static String base64toHex(String base64_input) {
- byte[] byteArray = Base64.decode(base64_input, Base64.DEFAULT);
+ byte[] byteArray = Base64.decode(base64_input);
int readBytes = byteArray.length;
StringBuffer hexData = new StringBuffer();
int onebyte;
@@ -172,6 +183,15 @@ public class ConfigHelper {
}
return hexData.toString();
}
+
+ @NonNull
+ public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ {
+ return base64toHex(
+ //new String(Base64.encode(MessageDigest.getInstance(encoding).digest(certificate.getEncoded())), "US-ASCII"));
+ android.util.Base64.encodeToString(
+ MessageDigest.getInstance(encoding).digest(certificate.getEncoded()),
+ android.util.Base64.DEFAULT));
+ }
/**
* Adds a new X509 certificate given its input stream and its provider name
diff --git a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
index 6f6a14de..9d3f4b52 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -16,19 +16,19 @@
*/
package se.leap.bitmaskclient;
-import android.app.*;
-import android.content.*;
-import android.os.*;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.os.Bundle;
-import org.json.JSONException;
import org.json.JSONObject;
-import se.leap.bitmaskclient.userstatus.SessionDialog;
-
import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.valueOf;
-import static se.leap.bitmaskclient.ProviderApiBase.ERRORID;
-import static se.leap.bitmaskclient.ProviderApiBase.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
/**
* Implements a dialog to show why a download failed.
diff --git a/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
new file mode 100644
index 00000000..1bf679f8
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Build;
+import android.support.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.ProviderAPI.ERRORS;
+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;
+
+/**
+ * Created by cyberta on 08.01.18.
+ */
+
+public class OkHttpClientGenerator {
+
+ SharedPreferences preferences;
+ Resources resources;
+
+ public OkHttpClientGenerator(SharedPreferences preferences, Resources resources) {
+ this.preferences = preferences;
+ this.resources = resources;
+ }
+
+ public OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
+ return initHttpClient(initError, null);
+ }
+
+ public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
+ String certificate = preferences.getString(Provider.CA_CERT, "");
+ return initHttpClient(initError, certificate);
+ }
+
+ public OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) {
+ return initHttpClient(initError, certificate);
+ }
+
+
+ private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
+ try {
+ 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));
+ return clientBuilder.build();
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ addErrorMessageToJson(initError, resources.getString(R.string.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();
+ addErrorMessageToJson(initError, resources.getString(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));
+ }
+ return null;
+ }
+
+
+
+ @NonNull
+ private ConnectionSpec getConnectionSpec() {
+ ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
+ //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels
+ //figure out how to use bcjsse for that purpose
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
+ connectionSpecbuilder.cipherSuites(
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
+ );
+ return connectionSpecbuilder.build();
+ }
+
+ @NonNull
+ private CookieJar getCookieJar() {
+ return new CookieJar() {
+ private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
+
+ @Override
+ public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
+ cookieStore.put(url.host(), cookies);
+ }
+
+ @Override
+ public List<Cookie> loadForRequest(HttpUrl url) {
+ List<Cookie> cookies = cookieStore.get(url.host());
+ return cookies != null ? cookies : new ArrayList<Cookie>();
+ }
+ };
+ }
+
+ private void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
new file mode 100644
index 00000000..5a6aabc0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPI.java
@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2017 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package se.leap.bitmaskclient;
+
+import android.annotation.SuppressLint;
+import android.app.IntentService;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import de.blinkt.openvpn.core.Preferences;
+
+/**
+ * Implements HTTP api methods (encapsulated in {{@link ProviderApiManager}})
+ * used to manage communications with the provider server.
+ * <p/>
+ * It's an IntentService because it downloads data from the Internet, so it operates in the background.
+ *
+ * @author parmegv
+ * @author MeanderingCode
+ * @author cyberta
+ */
+
+public class ProviderAPI extends IntentService implements ProviderApiManagerBase.ProviderApiServiceCallback {
+
+ final public static String
+ TAG = ProviderAPI.class.getSimpleName(),
+ SET_UP_PROVIDER = "setUpProvider",
+ UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
+ DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
+ SIGN_UP = "srpRegister",
+ LOG_IN = "srpAuth",
+ LOG_OUT = "logOut",
+ DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
+ PARAMETERS = "parameters",
+ RESULT_KEY = "result",
+ RECEIVER_KEY = "receiver",
+ ERRORS = "errors",
+ ERRORID = "errorId",
+ UPDATE_PROGRESSBAR = "update_progressbar",
+ CURRENT_PROGRESS = "current_progress",
+ DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE";
+
+ final public static int
+ SUCCESSFUL_LOGIN = 3,
+ FAILED_LOGIN = 4,
+ SUCCESSFUL_SIGNUP = 5,
+ FAILED_SIGNUP = 6,
+ SUCCESSFUL_LOGOUT = 7,
+ LOGOUT_FAILED = 8,
+ CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
+ INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
+ PROVIDER_OK = 11,
+ PROVIDER_NOK = 12,
+ CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
+ INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14;
+
+ ProviderApiManager providerApiManager;
+
+
+
+ public ProviderAPI() {
+ super(TAG);
+ }
+
+ //TODO: refactor me, please!
+ public static void stop() {
+ ProviderApiManager.stop();
+ }
+
+ //TODO: refactor me, please!
+ public static boolean caCertDownloaded() {
+ return ProviderApiManager.caCertDownloaded();
+ }
+
+ //TODO: refactor me, please!
+ public static String lastProviderMainUrl() {
+ return ProviderApiManager.lastProviderMainUrl();
+ }
+
+ //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();
+ }
+
+ @Override
+ public void broadcastProgress(Intent intent) {
+ sendBroadcast(intent);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent command) {
+ providerApiManager.handleIntent(command);
+ }
+
+
+ private ProviderApiManager initApiManager() {
+ SharedPreferences preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE);
+ OkHttpClientGenerator clientGenerator = new OkHttpClientGenerator(preferences, getResources());
+ return new ProviderApiManager(preferences, getResources(), clientGenerator, this);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
index 9b880f89..9b777e5a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderAPIResultReceiver.java
@@ -16,7 +16,9 @@
*/
package se.leap.bitmaskclient;
-import android.os.*;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.ResultReceiver;
/**
* Implements the ResultReceiver needed by Activities using ProviderAPI to receive the results of its operations.
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java
new file mode 100644
index 00000000..9aad14d5
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient;
+
+import android.support.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();
+ 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) {
+ try {
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method("GET", null);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ return response.isSuccessful();
+ } catch (RuntimeException | IOException e) {
+ return false;
+ }
+
+ }
+
+ public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException {
+
+ RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method(request_method, jsonBody);
+ if (headerArgs != null) {
+ for (Pair<String, String> keyValPair : headerArgs) {
+ requestBuilder.addHeader(keyValPair.first, keyValPair.second);
+ }
+ }
+ //TODO: move to getHeaderArgs()?
+ String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
+ requestBuilder.addHeader("Accept-Language", locale);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ InputStream inputStream = response.body().byteStream();
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ return scanner.next();
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index 0013d2c2..396d642b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
@@ -1,5 +1,5 @@
/**
- * Copyright (c) 2017 LEAP Encryption Access Project and contributers
+ * 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
@@ -14,13 +14,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
package se.leap.bitmaskclient;
-import android.app.IntentService;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
-import android.os.Build;
import android.os.Bundle;
import android.os.ResultReceiver;
import android.support.annotation.NonNull;
@@ -38,11 +37,7 @@ import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.net.UnknownServiceException;
-import java.security.KeyManagementException;
-import java.security.KeyStoreException;
-import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
@@ -50,90 +45,75 @@ import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import javax.net.ssl.SSLHandshakeException;
-import okhttp3.CipherSuite;
-import okhttp3.ConnectionSpec;
-import okhttp3.Cookie;
-import okhttp3.CookieJar;
-import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
-import okhttp3.TlsVersion;
import se.leap.bitmaskclient.userstatus.SessionDialog;
import se.leap.bitmaskclient.userstatus.User;
import se.leap.bitmaskclient.userstatus.UserStatus;
-import static android.text.TextUtils.isEmpty;
-import static se.leap.bitmaskclient.ConfigHelper.base64toHex;
+import static se.leap.bitmaskclient.ConfigHelper.getFingerprintFromCertificate;
import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_INVALID_CERTIFICATE;
import static se.leap.bitmaskclient.Provider.MAIN_URL;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.CORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.CURRENT_PROGRESS;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.DOWNLOAD_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.FAILED_LOGIN;
+import static se.leap.bitmaskclient.ProviderAPI.FAILED_SIGNUP;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_CERTIFICATE;
+import static se.leap.bitmaskclient.ProviderAPI.INCORRECTLY_DOWNLOADED_EIP_SERVICE;
+import static se.leap.bitmaskclient.ProviderAPI.LOGOUT_FAILED;
+import static se.leap.bitmaskclient.ProviderAPI.LOG_IN;
+import static se.leap.bitmaskclient.ProviderAPI.LOG_OUT;
+import static se.leap.bitmaskclient.ProviderAPI.PARAMETERS;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.ProviderAPI.RECEIVER_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
+import static se.leap.bitmaskclient.ProviderAPI.SET_UP_PROVIDER;
+import static se.leap.bitmaskclient.ProviderAPI.SIGN_UP;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGIN;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_LOGOUT;
+import static se.leap.bitmaskclient.ProviderAPI.SUCCESSFUL_SIGNUP;
+import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROGRESSBAR;
+import static se.leap.bitmaskclient.ProviderAPI.UPDATE_PROVIDER_DETAILS;
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.keyChainAccessError;
import static se.leap.bitmaskclient.R.string.malformed_url;
+import static se.leap.bitmaskclient.R.string.retry;
import static se.leap.bitmaskclient.R.string.server_unreachable_message;
import static se.leap.bitmaskclient.R.string.service_is_down_error;
/**
- * Implements HTTP api methods used to manage communications with the provider server.
- * The implemented methods are commonly used by insecure's and production's flavor of ProviderAPI.
- * <p/>
- * It's an IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Implements the logic of the http api calls. The methods of this class needs to be called from
+ * a background thread.
*/
-public abstract class ProviderApiBase extends IntentService {
-
- final public static String
- TAG = ProviderAPI.class.getSimpleName(),
- SET_UP_PROVIDER = "setUpProvider",
- UPDATE_PROVIDER_DETAILS = "updateProviderDetails",
- DOWNLOAD_NEW_PROVIDER_DOTJSON = "downloadNewProviderDotJSON",
- SIGN_UP = "srpRegister",
- LOG_IN = "srpAuth",
- LOG_OUT = "logOut",
- DOWNLOAD_CERTIFICATE = "downloadUserAuthedCertificate",
- PARAMETERS = "parameters",
- RESULT_KEY = "result",
- RECEIVER_KEY = "receiver",
- ERRORS = "errors",
- ERRORID = "errorId",
- UPDATE_PROGRESSBAR = "update_progressbar",
- CURRENT_PROGRESS = "current_progress",
- DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE";
-
- final public static int
- SUCCESSFUL_LOGIN = 3,
- FAILED_LOGIN = 4,
- SUCCESSFUL_SIGNUP = 5,
- FAILED_SIGNUP = 6,
- SUCCESSFUL_LOGOUT = 7,
- LOGOUT_FAILED = 8,
- CORRECTLY_DOWNLOADED_CERTIFICATE = 9,
- INCORRECTLY_DOWNLOADED_CERTIFICATE = 10,
- PROVIDER_OK = 11,
- PROVIDER_NOK = 12,
- CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
- INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14;
-
- protected static boolean
+public abstract class ProviderApiManagerBase {
+
+ public interface ProviderApiServiceCallback {
+ void broadcastProgress(Intent intent);
+ }
+
+ private ProviderApiServiceCallback serviceCallback;
+
+ protected static volatile boolean
CA_CERT_DOWNLOADED = false,
PROVIDER_JSON_DOWNLOADED = false,
EIP_SERVICE_JSON_DOWNLOADED = false;
@@ -146,32 +126,28 @@ public abstract class ProviderApiBase extends IntentService {
protected static String providerCaCert;
protected static JSONObject providerDefinition;
protected Resources resources;
+ protected OkHttpClientGenerator clientGenerator;
public static void stop() {
go_ahead = false;
}
- private final MediaType JSON
- = MediaType.parse("application/json; charset=utf-8");
-
- public ProviderApiBase() {
- super(TAG);
+ public static String lastProviderMainUrl() {
+ return lastProviderMainUrl;
}
- @Override
- public void onCreate() {
- super.onCreate();
- preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE);
- resources = getResources();
- }
+ private final MediaType JSON
+ = MediaType.parse("application/json; charset=utf-8");
- public static String lastProviderMainUrl() {
- return lastProviderMainUrl;
+ public ProviderApiManagerBase(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ this.preferences = preferences;
+ this.resources = resources;
+ this.serviceCallback = callback;
+ this.clientGenerator = clientGenerator;
}
- @Override
- protected void onHandleIntent(Intent command) {
+ public void handleIntent(Intent command) {
final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY);
String action = command.getAction();
Bundle parameters = command.getBundleExtra(PARAMETERS);
@@ -185,6 +161,7 @@ public abstract class ProviderApiBase extends IntentService {
go_ahead = false;
}
}
+
if (action.equals(UPDATE_PROVIDER_DETAILS)) {
resetProviderDetails();
Bundle task = new Bundle();
@@ -255,7 +232,7 @@ public abstract class ProviderApiBase extends IntentService {
}
protected String formatErrorMessage(final int toastStringId) {
- return formatErrorMessage(getResources().getString(toastStringId));
+ return formatErrorMessage(resources.getString(toastStringId));
}
private String formatErrorMessage(String errorMessage) {
@@ -288,91 +265,7 @@ public abstract class ProviderApiBase extends IntentService {
}
}
- private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
- try {
- 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));
- return clientBuilder.build();
- } catch (IllegalArgumentException e) {
- e.printStackTrace();
- addErrorMessageToJson(initError, resources.getString(R.string.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();
- addErrorMessageToJson(initError, resources.getString(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));
- }
- return null;
- }
-
- protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, null);
- }
-
- protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
- String certificate = preferences.getString(Provider.CA_CERT, "");
- return initHttpClient(initError, certificate);
- }
-
- protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) {
- return initHttpClient(initError, certificate);
- }
-
- @NonNull
- private ConnectionSpec getConnectionSpec() {
- ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
- .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3);
- //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels
- //figure out how to use bcjsse for that purpose
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1)
- connectionSpecbuilder.cipherSuites(
- CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
- CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
- );
- return connectionSpecbuilder.build();
- }
-
- @NonNull
- private CookieJar getCookieJar() {
- return new CookieJar() {
- private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();
-
- @Override
- public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
- cookieStore.put(url.host(), cookies);
- }
-
- @Override
- public List<Cookie> loadForRequest(HttpUrl url) {
- List<Cookie> cookies = cookieStore.get(url.host());
- return cookies != null ? cookies : new ArrayList<Cookie>();
- }
- };
- }
private Bundle tryToRegister(Bundle task) {
@@ -402,7 +295,7 @@ public abstract class ProviderApiBase extends IntentService {
private Bundle register(String username, String password) {
JSONObject stepResult = null;
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult);
if (okHttpClient == null) {
return authFailedNotification(stepResult, username);
}
@@ -459,7 +352,7 @@ public abstract class ProviderApiBase extends IntentService {
private Bundle authenticate(String username, String password) {
Bundle result = new Bundle();
JSONObject stepResult = new JSONObject();
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(stepResult);
if (okHttpClient == null) {
return authFailedNotification(stepResult, username);
}
@@ -544,7 +437,8 @@ public abstract class ProviderApiBase extends IntentService {
intentUpdate.setAction(UPDATE_PROGRESSBAR);
intentUpdate.addCategory(Intent.CATEGORY_DEFAULT);
intentUpdate.putExtra(CURRENT_PROGRESS, progress);
- sendBroadcast(intentUpdate);
+ serviceCallback.broadcastProgress(intentUpdate);
+ //sendBroadcast(intentUpdate);
}
/**
@@ -646,10 +540,10 @@ public abstract class ProviderApiBase extends IntentService {
}
private String requestStringFromServer(String url, String request_method, String jsonString, List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
- Response response;
+ //Response response;
String plainResponseBody = null;
- RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
+ /*RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
Request.Builder requestBuilder = new Request.Builder()
.url(url)
.method(request_method, jsonBody);
@@ -662,16 +556,17 @@ public abstract class ProviderApiBase extends IntentService {
String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
requestBuilder.addHeader("Accept-Language", locale);
Request request = requestBuilder.build();
-
+*/
try {
- response = okHttpClient.newCall(request).execute();
-
- InputStream inputStream = response.body().byteStream();
- Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
- if (scanner.hasNext()) {
- plainResponseBody = scanner.next();
- }
-
+ //response = okHttpClient.newCall(request).execute();
+ //response = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
+ //InputStream inputStream = response.body().byteStream();
+ //Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ //if (scanner.hasNext()) {
+ // plainResponseBody = scanner.next();
+ //}
+
+ plainResponseBody = ProviderApiConnector.requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
} catch (NullPointerException npe) {
plainResponseBody = formatErrorMessage(error_json_exception_user_message);
} catch (UnknownHostException | SocketTimeoutException e) {
@@ -742,14 +637,12 @@ public abstract class ProviderApiBase extends IntentService {
String fingerprint = provider_json.getString(Provider.CA_CERT_FINGERPRINT);
String encoding = fingerprint.split(":")[0];
String expected_fingerprint = fingerprint.split(":")[1];
- String real_fingerprint = base64toHex(Base64.encodeToString(
- MessageDigest.getInstance(encoding).digest(certificate.getEncoded()),
- Base64.DEFAULT));
+ String real_fingerprint = getFingerprintFromCertificate(certificate, encoding);
result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim());
} else
result = false;
- } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) {
+ } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException /*| UnsupportedEncodingException*/ e) {
result = false;
}
}
@@ -757,8 +650,11 @@ public abstract class ProviderApiBase extends IntentService {
return result;
}
+
+
protected void checkPersistedProviderUpdates() {
- String providerDomain = getProviderDomain(providerDefinition);
+ //String providerDomain = getProviderDomain(providerDefinition);
+ String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
if (hasUpdatedProviderDetails(providerDomain)) {
providerCaCert = getPersistedProviderCA(providerDomain);
providerDefinition = getPersistedProviderDefinition(providerDomain);
@@ -787,7 +683,7 @@ public abstract class ProviderApiBase extends IntentService {
} catch (JSONException e) {
e.printStackTrace();
result.putBoolean(RESULT_KEY, false);
- result = setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ result = setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
}
return result;
@@ -803,22 +699,21 @@ public abstract class ProviderApiBase extends IntentService {
X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string);
if (certificate == null) {
- return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString());
+ return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_INVALID_CERTIFICATE.toString());
}
try {
certificate.checkValidity();
String fingerprint = getCaCertFingerprint(providerDefinition);
String encoding = fingerprint.split(":")[0];
String expected_fingerprint = fingerprint.split(":")[1];
- String real_fingerprint = base64toHex(Base64.encodeToString(
- MessageDigest.getInstance(encoding).digest(certificate.getEncoded()),
- Base64.DEFAULT));
+ String real_fingerprint = getFingerprintFromCertificate(certificate, encoding);
if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) {
- return setErrorResult(result, getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString());
+ return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString());
}
+
if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){
- return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
}
if (!canConnect(cert_string, providerDefinition, result)) {
@@ -827,10 +722,12 @@ public abstract class ProviderApiBase extends IntentService {
} catch (NoSuchAlgorithmException e ) {
return setErrorResult(result, resources.getString(error_no_such_algorithm_exception_user_message), null);
} catch (ArrayIndexOutOfBoundsException e) {
- return setErrorResult(result, getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_details), ERROR_CORRUPTED_PROVIDER_JSON.toString());
} catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
- return setErrorResult(result, getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString());
- }
+ return setErrorResult(result, resources.getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString());
+ } /*catch (UnsupportedEncodingException e) {
+ return setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString());
+ }*/
result.putBoolean(RESULT_KEY, true);
return result;
@@ -870,13 +767,23 @@ public abstract class ProviderApiBase extends IntentService {
JSONObject errorJson = new JSONObject();
String baseUrl = getApiUrl(providerDefinition);
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
if (okHttpClient == null) {
result.putString(ERRORS, errorJson.toString());
return false;
}
- List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+ //try {
+
+ return ProviderApiConnector.canConnect(okHttpClient, baseUrl);
+ /*} catch (RuntimeException | IOException e) {
+ e.printStackTrace();
+ }*/
+
+ //return false;
+
+
+ /*List<Pair<String, String>> headerArgs = getAuthorizationHeader();
String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient);
try {
@@ -888,7 +795,7 @@ public abstract class ProviderApiBase extends IntentService {
//eat me
}
- return true;
+ return true;*/
}
protected String getCaCertFingerprint(JSONObject providerDefinition) {
@@ -964,8 +871,13 @@ public abstract class ProviderApiBase extends IntentService {
return "";
}
- protected boolean hasUpdatedProviderDetails(String providerDomain) {
- return preferences.contains(Provider.KEY + "." + providerDomain) && preferences.contains(Provider.CA_CERT + "." + providerDomain);
+ protected boolean hasUpdatedProviderDetails(String domain) {
+ return preferences.contains(Provider.KEY + "." + domain) && preferences.contains(Provider.CA_CERT + "." + domain);
+ }
+
+ protected String getDomainFromMainURL(String mainUrl) {
+ return mainUrl.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+
}
/**
@@ -983,6 +895,8 @@ public abstract class ProviderApiBase extends IntentService {
} catch (JSONException e) {
// TODO Auto-generated catch block
error_message = string_json_error_message;
+ } catch (NullPointerException e) {
+ //do nothing
}
return error_message;
@@ -999,7 +913,7 @@ public abstract class ProviderApiBase extends IntentService {
}
private boolean logOut() {
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject());
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(new JSONObject());
if (okHttpClient == null) {
return false;
}
@@ -1007,23 +921,32 @@ public abstract class ProviderApiBase extends IntentService {
String deleteUrl = providerApiUrl + "/logout";
int progress = 0;
- Request.Builder requestBuilder = new Request.Builder()
+ /* Request.Builder requestBuilder = new Request.Builder()
.url(deleteUrl)
.delete();
- Request request = requestBuilder.build();
-
- try {
- Response response = okHttpClient.newCall(request).execute();
- // v---- was already not authorized
- if (response.isSuccessful() || response.code() == 401) {
- broadcastProgress(progress++);
- LeapSRPSession.setToken("");
- }
-
- } catch (IOException e) {
- return false;
+ Request request = requestBuilder.build();*/
+
+ //try {
+
+ //Response response = okHttpClient.newCall(request).execute();
+// Response response = ProviderApiConnector.delete(okHttpClient, deleteUrl);
+// // v---- was already not authorized
+// if (response.isSuccessful() || response.code() == 401) {
+// broadcastProgress(progress++);
+// LeapSRPSession.setToken("");
+// }
+//
+// } catch (IOException | RuntimeException e) {
+// return false;
+// }
+// return true;
+
+ if (ProviderApiConnector.delete(okHttpClient, deleteUrl)) {
+ broadcastProgress(progress++);
+ LeapSRPSession.setToken("");
+ return true;
}
- return true;
+ return false;
}
//FIXME: don't save private keys in shared preferences! use the keystore
diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
index 61349490..bd9324bb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java
@@ -23,8 +23,6 @@ import android.view.*;
import android.widget.*;
import butterknife.*;
-import se.leap.bitmaskclient.ProviderAPI;
-import se.leap.bitmaskclient.VpnFragment;
import se.leap.bitmaskclient.Provider;
import se.leap.bitmaskclient.R;
diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
index 39651a43..31c63d01 100644
--- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java
+++ b/app/src/production/java/se/leap/bitmaskclient/ProviderApiManager.java
@@ -1,6 +1,5 @@
-
/**
- * Copyright (c) 2013 LEAP Encryption Access Project and contributers
+ * 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
@@ -15,8 +14,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+
package se.leap.bitmaskclient;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
import android.os.Bundle;
import android.util.Pair;
@@ -30,21 +32,30 @@ import java.util.List;
import okhttp3.OkHttpClient;
import se.leap.bitmaskclient.eip.EIP;
-import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CORRUPTED_PROVIDER_JSON;
+import static android.text.TextUtils.isEmpty;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.ERROR_CERTIFICATE_PINNING;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
import static se.leap.bitmaskclient.R.string.malformed_url;
/**
- * Implements HTTP api methods used to manage communications with the provider server.
- * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors
- * of ProviderAPI.
- * <p/>
- * It extends an IntentService because it downloads data from the Internet, so it operates in the background.
- *
- * @author parmegv
- * @author MeanderingCode
- * @author cyberta
+ * Implements the logic of the provider api http requests. The methods of this class need to be called from
+ * a background thread.
*/
-public class ProviderAPI extends ProviderApiBase {
+
+
+public class ProviderApiManager extends ProviderApiManagerBase {
+
+ public ProviderApiManager(SharedPreferences preferences, Resources resources, OkHttpClientGenerator clientGenerator, ProviderApiServiceCallback callback) {
+ super(preferences, resources, clientGenerator, callback);
+ }
+
+ /**
+ * Only used in insecure flavor.
+ */
+ static boolean lastDangerOn() {
+ return false;
+ }
/**
* Downloads a provider.json from a given URL, adding a new provider using the given name.
@@ -62,6 +73,13 @@ public class ProviderAPI extends ProviderApiBase {
lastProviderMainUrl = task.containsKey(Provider.MAIN_URL) ?
task.getString(Provider.MAIN_URL) :
"";
+
+ if (isEmpty(lastProviderMainUrl)) {
+ currentDownload.putBoolean(RESULT_KEY, false);
+ setErrorResult(currentDownload, resources.getString(R.string.malformed_url), null);
+ return currentDownload;
+ }
+
//TODO: remove that
providerCaCertFingerprint = task.containsKey(Provider.CA_CERT_FINGERPRINT) ?
task.getString(Provider.CA_CERT_FINGERPRINT) :
@@ -131,17 +149,17 @@ public class ProviderAPI extends ProviderApiBase {
providerDotJsonString = downloadFromApiUrlWithProviderCA("/provider.json", caCert, providerDefinition);
}
- if (!isValidJson(providerDotJsonString)) {
- result.putString(ERRORS, getString(malformed_url));
- result.putBoolean(RESULT_KEY, false);
- return result;
- }
+ if (!isValidJson(providerDotJsonString)) {
+ result.putString(ERRORS, resources.getString(malformed_url));
+ result.putBoolean(RESULT_KEY, false);
+ return result;
+ }
try {
JSONObject providerJson = new JSONObject(providerDotJsonString);
- String providerDomain = providerJson.getString(Provider.DOMAIN);
+ String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
providerApiUrl = getApiUrlWithVersion(providerJson);
- String name = providerJson.getString(Provider.NAME);
+ //String name = providerJson.getString(Provider.NAME);
//TODO setProviderName(name);
preferences.edit().putString(Provider.KEY, providerJson.toString()).
@@ -221,8 +239,7 @@ public class ProviderAPI extends ProviderApiBase {
try {
JSONObject providerJson = new JSONObject(preferences.getString(Provider.KEY, ""));
String caCertUrl = providerJson.getString(Provider.CA_CERT_URI);
- String providerDomain = providerJson.getString(Provider.DOMAIN);
-
+ String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
String cert_string = downloadWithCommercialCA(caCertUrl);
if (validCertificate(cert_string) && go_ahead) {
@@ -230,13 +247,11 @@ public class ProviderAPI extends ProviderApiBase {
preferences.edit().putString(Provider.CA_CERT + "." + providerDomain, cert_string).commit();
result.putBoolean(RESULT_KEY, true);
} else {
- String reason_to_fail = pickErrorMessage(cert_string);
- result.putString(ERRORS, reason_to_fail);
+ setErrorResult(result, resources.getString(R.string.warning_corrupted_provider_cert), ERROR_CERTIFICATE_PINNING.toString());
result.putBoolean(RESULT_KEY, false);
}
} catch (JSONException e) {
- String reason_to_fail = formatErrorMessage(malformed_url);
- result.putString(ERRORS, reason_to_fail);
+ setErrorResult(result, resources.getString(malformed_url), null);
result.putBoolean(RESULT_KEY, false);
}
@@ -249,11 +264,11 @@ public class ProviderAPI extends ProviderApiBase {
* @param string_url
* @return
*/
- protected String downloadWithCommercialCA(String string_url) {
+ private String downloadWithCommercialCA(String string_url) {
String responseString;
JSONObject errorJson = new JSONObject();
- OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson);
+ OkHttpClient okHttpClient = clientGenerator.initCommercialCAHttpClient(errorJson);
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -266,7 +281,7 @@ public class ProviderAPI extends ProviderApiBase {
try {
// try to download with provider CA on certificate error
JSONObject responseErrorJson = new JSONObject(responseString);
- if (responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) {
+ if (responseErrorJson.getString(ERRORS).equals(resources.getString(R.string.certificate_error))) {
responseString = downloadWithProviderCA(string_url);
}
} catch (JSONException e) {
@@ -287,7 +302,7 @@ public class ProviderAPI extends ProviderApiBase {
String responseString;
JSONObject errorJson = new JSONObject();
String baseUrl = getApiUrl(providerDefinition);
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
if (okHttpClient == null) {
return errorJson.toString();
}
@@ -301,18 +316,17 @@ public class ProviderAPI extends ProviderApiBase {
}
-
/**
* Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider.
*
* @param urlString as a string
* @return an empty string if it fails, the url content if not.
*/
- protected String downloadWithProviderCA(String urlString) {
+ private String downloadWithProviderCA(String urlString) {
JSONObject initError = new JSONObject();
String responseString;
- OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError);
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(initError);
if (okHttpClient == null) {
return initError.toString();
}
@@ -323,5 +337,4 @@ public class ProviderAPI extends ProviderApiBase {
return responseString;
}
-
}
diff --git a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java b/app/src/test/java/se/leap/bitmaskclient/TestUtils.java
deleted file mode 100644
index 96b11df6..00000000
--- a/app/src/test/java/se/leap/bitmaskclient/TestUtils.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package se.leap.bitmaskclient;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-
-/**
- * Created by cyberta on 08.10.17.
- */
-
-public class TestUtils {
-
- public static String getInputAsString(InputStream fileAsInputStream) throws IOException {
- BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream));
- StringBuilder sb = new StringBuilder();
- String line = br.readLine();
- while (line != null) {
- sb.append(line);
- line = br.readLine();
- }
-
- return sb.toString();
- }
-
-}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
index ea212480..4726cab7 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java
@@ -16,7 +16,7 @@ import java.io.IOException;
import se.leap.bitmaskclient.Constants;
import se.leap.bitmaskclient.Provider;
-import se.leap.bitmaskclient.TestUtils;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
import static junit.framework.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -61,17 +61,17 @@ public class GatewaysManagerTest {
@Test
public void testFromEipServiceJson_ignoreDuplicateGateways() throws Exception {
- String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
+ String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
assertEquals(2, gatewaysManager.size());
- eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json"));
+ eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-one-gateway.json"));
gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
assertEquals(2, gatewaysManager.size());
}
@Test
public void testClearGatewaysAndProfiles_resetGateways() throws Exception {
- String eipServiceJson = TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
+ String eipServiceJson = TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("eip-service-two-gateways.json"));
gatewaysManager.fromEipServiceJson(new JSONObject(eipServiceJson));
assertEquals(2, gatewaysManager.size());
gatewaysManager.clearGatewaysAndProfiles();
@@ -79,7 +79,7 @@ public class GatewaysManagerTest {
}
private String getJsonStringFor(String filename) throws IOException {
- return TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
+ return TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream(filename));
}
} \ No newline at end of file
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
new file mode 100644
index 00000000..c39681c4
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/ProviderApiManagerTest.java
@@ -0,0 +1,249 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.eip;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Answers;
+import org.mockito.Mock;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.Provider;
+import se.leap.bitmaskclient.ProviderAPI;
+import se.leap.bitmaskclient.ProviderApiConnector;
+import se.leap.bitmaskclient.ProviderApiManager;
+import se.leap.bitmaskclient.ProviderApiManagerBase;
+import se.leap.bitmaskclient.testutils.MockSharedPreferences;
+
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_NOK;
+import static se.leap.bitmaskclient.ProviderAPI.PROVIDER_OK;
+import static se.leap.bitmaskclient.ProviderAPI.RESULT_KEY;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockBundle;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockClientGenerator;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockFingerprintForCertificate;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockIntent;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockProviderApiConnector;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResources;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockResultReceiver;
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.mockTextUtils;
+import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.NO_ERROR;
+
+/**
+ * Created by cyberta on 04.01.18.
+ */
+
+@RunWith(PowerMockRunner.class)
+@PrepareForTest({ProviderApiManager.class, TextUtils.class, ConfigHelper.class, ProviderApiConnector.class})
+public class ProviderApiManagerTest {
+
+ private SharedPreferences mockPreferences;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private Resources mockResources;
+ @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+ private Context mockContext;
+
+ private ProviderApiManager providerApiManager;
+
+ class TestProviderApiServiceCallback implements ProviderApiManagerBase.ProviderApiServiceCallback {
+
+ //Intent expectedIntent;
+ TestProviderApiServiceCallback(/*Intent expectedIntent*/) {
+ //this.expectedIntent = expectedIntent;
+ }
+
+ @Override
+ public void broadcastProgress(Intent intent) {
+ //assertEquals("expected intent: ", expectedIntent, intent);
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+
+ Bundle bundle = mockBundle();
+ PowerMockito.whenNew(Bundle.class).withAnyArguments().thenReturn(bundle);
+ Intent intent = mockIntent();
+ PowerMockito.whenNew(Intent.class).withAnyArguments().thenReturn(intent);
+ mockTextUtils();
+ mockPreferences = new MockSharedPreferences();
+ mockResources = mockResources(getClass().getClassLoader().getResourceAsStream("error_messages.json"));
+ }
+
+
+ @Test
+ public void test_handleIntentSetupProvider_noProviderMainURL() {
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errors\":\"It doesn't seem to be a Bitmask provider.\"}");
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "");
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_happyPath_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, true);
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+ parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")));
+ parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_happyPath_no_preseededProviderAndCA() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, true);
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_happyPath_storedProviderAndCAFromPreviousSetup() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494");
+ mockProviderApiConnector(NO_ERROR);
+ mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+ mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, true);
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_OK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_preseededProviderAndCA_failedCAPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate(" a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+ mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+ parameters.putString(Provider.CA_CERT, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem")));
+ parameters.putString(Provider.KEY, getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json")));
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_no_preseededProviderAndCA_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+ mockProviderApiConnector(NO_ERROR);
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+ @Test
+ public void test_handleIntentSetupProvider_storedProviderAndCAFromPreviousSetup_failedPinning() throws IOException, CertificateEncodingException, NoSuchAlgorithmException {
+ mockFingerprintForCertificate("a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29495");
+ mockProviderApiConnector(NO_ERROR);
+ mockPreferences.edit().putString(Provider.KEY + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"))).apply();
+ mockPreferences.edit().putString(Provider.CA_CERT + ".riseup.net", getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"))).apply();
+ providerApiManager = new ProviderApiManager(mockPreferences, mockResources, mockClientGenerator(), new TestProviderApiServiceCallback());
+ Bundle expectedResult = mockBundle();
+ expectedResult.putBoolean(RESULT_KEY, false);
+ expectedResult.putString(ERRORS, "{\"errorId\":\"ERROR_CERTIFICATE_PINNING\",\"errors\":\"Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.\"}");
+
+ Intent provider_API_command = mockIntent();
+ Bundle parameters = mockBundle();
+ parameters.putString(Provider.MAIN_URL, "https://riseup.net");
+
+ provider_API_command.setAction(ProviderAPI.SET_UP_PROVIDER);
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, mockResultReceiver(PROVIDER_NOK, expectedResult));
+
+ providerApiManager.handleIntent(provider_API_command);
+ }
+
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
index 7e60edb9..8c8cdb61 100644
--- a/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
+++ b/app/src/test/java/se/leap/bitmaskclient/eip/VpnConfigGeneratorTest.java
@@ -4,7 +4,7 @@ import org.json.JSONObject;
import org.junit.Before;
import org.junit.Test;
-import se.leap.bitmaskclient.TestUtils;
+import se.leap.bitmaskclient.testutils.TestSetupHelper;
import static junit.framework.Assert.assertTrue;
@@ -217,13 +217,13 @@ public class VpnConfigGeneratorTest {
@Before
public void setUp() throws Exception {
- generalConfig = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json")));
- secrets = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json")));
+ generalConfig = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("general_configuration.json")));
+ secrets = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("secrets.json")));
}
@Test
public void testGenerate_tcp_udp() throws Exception {
- gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_tcp_udp.json")));
vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
String vpnConfig = vpnConfigGenerator.generate();
@@ -232,7 +232,7 @@ public class VpnConfigGeneratorTest {
@Test
public void testGenerate_udp_tcp() throws Exception {
- gateway = new JSONObject(TestUtils.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
+ gateway = new JSONObject(TestSetupHelper.getInputAsString(getClass().getClassLoader().getResourceAsStream("gateway_udp_tcp.json")));
vpnConfigGenerator = new VpnConfigGenerator(generalConfig, secrets, gateway);
String vpnConfig = vpnConfigGenerator.generate();
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
new file mode 100644
index 00000000..af35af31
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/MockSharedPreferences.java
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils;
+
+import android.content.SharedPreferences;
+import android.support.annotation.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class MockSharedPreferences implements SharedPreferences {
+ HashMap<String, String> mockedStringPrefs = new HashMap<>();
+ HashMap<String, Integer> mockedIntPrefs = new HashMap<>();
+ HashMap<String, Boolean> mockedBooleanPrefs = new HashMap<>();
+
+ @Override
+ public Map<String, ?> getAll() {
+ return null;
+ }
+
+ @Nullable
+ @Override
+ public String getString(String key, @Nullable String defValue) {
+ String value = mockedStringPrefs.get(key);
+ return value != null ? value : defValue;
+ }
+
+ @Nullable
+ @Override
+ public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+ return null;
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ Integer value = mockedIntPrefs.get(key);
+ return value != null ? value : defValue;
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ return 0;
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ return 0;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ Boolean value = mockedBooleanPrefs.get(key);
+ return value != null ? value : defValue;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return mockedStringPrefs.containsKey(key) ||
+ mockedBooleanPrefs.containsKey(key) ||
+ mockedIntPrefs.containsKey(key);
+ }
+
+ @Override
+ public Editor edit() {
+ return new Editor() {
+ private HashMap<String, String> tempStrings = new HashMap<>(mockedStringPrefs);
+ private HashMap<String, Integer> tempIntegers = new HashMap<>(mockedIntPrefs);
+ private HashMap<String, Boolean> tempBoolean = new HashMap<>(mockedBooleanPrefs);
+
+ @Override
+ public Editor putString(String key, @Nullable String value) {
+ tempStrings.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putStringSet(String key, @Nullable Set<String> values) {
+ return null;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ tempIntegers.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ return null;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ return null;
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ tempBoolean.put(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ tempBoolean.remove(key);
+ tempStrings.remove(key);
+ tempIntegers.remove(key);
+ return this;
+ }
+
+ @Override
+ public Editor clear() {
+ tempBoolean.clear();
+ tempStrings.clear();
+ tempIntegers.clear();
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ mockedStringPrefs = new HashMap<>(tempStrings);
+ mockedBooleanPrefs = new HashMap<>(tempBoolean);
+ mockedIntPrefs = new HashMap<>(tempIntegers);
+ return true;
+ }
+
+ @Override
+ public void apply() {
+ mockedStringPrefs = new HashMap<>(tempStrings);
+ mockedBooleanPrefs = new HashMap<>(tempBoolean);
+ mockedIntPrefs = new HashMap<>(tempIntegers);
+ }
+ };
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
+
+ }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
new file mode 100644
index 00000000..19d7e13f
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/TestSetupHelper.java
@@ -0,0 +1,443 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils;
+
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mockito.ArgumentMatchers;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import okhttp3.OkHttpClient;
+import se.leap.bitmaskclient.ConfigHelper;
+import se.leap.bitmaskclient.OkHttpClientGenerator;
+import se.leap.bitmaskclient.ProviderApiConnector;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric;
+import se.leap.bitmaskclient.testutils.matchers.BundleMatcher;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.powermock.api.mockito.PowerMockito.mockStatic;
+import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.TestBackendErrorCase.ERROR_NO_CONNECTION;
+import static se.leap.bitmaskclient.testutils.answers.BackendAnswerFabric.getAnswerForErrorcase;
+
+/**
+ * Created by cyberta on 08.10.17.
+ */
+
+public class TestSetupHelper {
+
+
+
+ public static String getInputAsString(InputStream fileAsInputStream) throws IOException {
+ BufferedReader br = new BufferedReader(new InputStreamReader(fileAsInputStream));
+ StringBuilder sb = new StringBuilder();
+ String line = br.readLine();
+ while (line != null) {
+ sb.append(line);
+ line = br.readLine();
+ }
+
+ return sb.toString();
+ }
+
+ @NonNull
+ public static Bundle mockBundle() {
+ final Map<String, Boolean> fakeBooleanBundle = new HashMap<>();
+ final Map<String, String> fakeStringBundle = new HashMap<>();
+ final Map<String, Integer> fakeIntBundle = new HashMap<>();
+ final Map<String, Parcelable> fakeParcelableBundle = new HashMap<>();
+
+ Bundle bundle = mock(Bundle.class);
+
+ //mock String values in Bundle
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ String value = ((String) arguments[1]);
+ fakeStringBundle.put(key, value);
+ return null;
+ }
+ }).when(bundle).putString(anyString(), anyString());
+ when(bundle.getString(anyString())).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return fakeStringBundle.get(key);
+ }
+ });
+
+ //mock Boolean values in Bundle
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ Boolean value = ((boolean) arguments[1]);
+ fakeBooleanBundle.put(key, value);
+ return null;
+ }
+ }).when(bundle).putBoolean(anyString(), anyBoolean());
+ when(bundle.getBoolean(anyString())).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return fakeBooleanBundle.get(key);
+ }
+ });
+
+ //mock Integer values in Bundle
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ Integer value = ((int) arguments[1]);
+ fakeIntBundle.put(key, value);
+ return null;
+ }
+ }).when(bundle).putInt(anyString(), anyInt());
+ when(bundle.getInt(anyString())).thenAnswer(new Answer<Integer>() {
+ @Override
+ public Integer answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return fakeIntBundle.get(key);
+ }
+ });
+
+ //mock Parcelable values in Bundle
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ Parcelable value = ((Parcelable) arguments[1]);
+ fakeParcelableBundle.put(key, value);
+ return null;
+ }
+ }).when(bundle).putParcelable(anyString(), any(Parcelable.class));
+ when(bundle.getParcelable(anyString())).thenAnswer(new Answer<Parcelable>() {
+ @Override
+ public Parcelable answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return fakeParcelableBundle.get(key);
+ }
+ });
+
+ //mock get
+ when(bundle.get(anyString())).thenAnswer(new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ if (fakeBooleanBundle.containsKey(key)) {
+ return fakeBooleanBundle.get(key);
+ } else if (fakeIntBundle.containsKey(key)) {
+ return fakeIntBundle.get(key);
+ } else if (fakeStringBundle.containsKey(key)) {
+ return fakeStringBundle.get(key);
+ } else {
+ return fakeParcelableBundle.get(key);
+ }
+ }
+ });
+
+ //mock getKeySet
+ when(bundle.keySet()).thenAnswer(new Answer<Set<String>>() {
+ @Override
+ public Set<String> answer(InvocationOnMock invocation) throws Throwable {
+ //this whole approach as a drawback:
+ //you should not add the same keys for values of different types
+ HashSet<String> keys = new HashSet<String>();
+ keys.addAll(fakeBooleanBundle.keySet());
+ keys.addAll(fakeIntBundle.keySet());
+ keys.addAll(fakeStringBundle.keySet());
+ keys.addAll(fakeParcelableBundle.keySet());
+ return keys;
+ }
+ });
+
+ //mock containsKey
+ when(bundle.containsKey(anyString())).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ String key = (String) invocation.getArguments()[0];
+ return fakeBooleanBundle.containsKey(key) ||
+ fakeStringBundle.containsKey(key) ||
+ fakeIntBundle.containsKey(key) ||
+ fakeParcelableBundle.containsKey(key);
+ }
+ });
+
+ return bundle;
+ }
+
+ public static Intent mockIntent() {
+ Intent intent = mock(Intent.class);
+ final String[] action = new String[1];
+ final Map<String, Object> fakeExtras = new HashMap<>();
+ final List<String> categories = new ArrayList<>();
+
+
+ //mock Action in intent
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ action[0] = ((String) arguments[0]);
+ return null;
+ }
+ }).when(intent).setAction(anyString());
+ when(intent.getAction()).thenAnswer(new Answer<String>() {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ return action[0];
+ }
+ });
+
+ //mock Bundle in intent extras
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ Bundle value = ((Bundle) arguments[1]);
+ fakeExtras.put(key, value);
+ return null;
+ }
+ }).when(intent).putExtra(anyString(), any(Bundle.class));
+ when(intent.getBundleExtra(anyString())).thenAnswer(new Answer<Bundle>() {
+ @Override
+ public Bundle answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return (Bundle) fakeExtras.get(key);
+ }
+ });
+
+ //mock Parcelable in intent extras
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ Parcelable value = ((Parcelable) arguments[1]);
+ fakeExtras.put(key, value);
+ return null;
+ }
+ }).when(intent).putExtra(anyString(), any(Parcelable.class));
+ when(intent.getParcelableExtra(anyString())).thenAnswer(new Answer<Parcelable>() {
+ @Override
+ public Parcelable answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ String key = ((String) arguments[0]);
+ return (Parcelable) fakeExtras.get(key);
+ }
+ });
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ categories.add(((String) arguments[0]));
+ return null;
+ }
+ }).when(intent).addCategory(anyString());
+
+ when(intent.getCategories()).thenAnswer(new Answer<Set<String>>() {
+ @Override
+ public Set<String> answer(InvocationOnMock invocation) throws Throwable {
+ return new HashSet<>(categories);
+ }
+ });
+
+ return intent;
+ }
+
+ public static void mockTextUtils() {
+ mockStatic(TextUtils.class);
+
+ when(TextUtils.equals(any(CharSequence.class), any(CharSequence.class))).thenAnswer(new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ CharSequence a = (CharSequence) invocation.getArguments()[0];
+ CharSequence b = (CharSequence) invocation.getArguments()[1];
+ if (a == b) return true;
+ int length;
+ if (a != null && b != null && (length = a.length()) == b.length()) {
+ if (a instanceof String && b instanceof String) {
+ return a.equals(b);
+ } else {
+ for (int i = 0; i < length; i++) {
+ if (a.charAt(i) != b.charAt(i)) return false;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+
+ when(TextUtils.isEmpty(any(CharSequence.class))).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocation) throws Throwable {
+ CharSequence param = (CharSequence) invocation.getArguments()[0];
+ return param == null || param.length() == 0;
+ }
+ });
+ }
+
+
+ public static ResultReceiver mockResultReceiver(final int expectedResultCode, final Bundle expectedBundle) {
+ ResultReceiver resultReceiver = mock(ResultReceiver.class);
+
+ doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object[] arguments = invocation.getArguments();
+ int resultCode = (int) arguments[0];
+ Bundle bundle = (Bundle) arguments[1];
+ Set<String> keys = expectedBundle.keySet();
+ Iterator<String> iterator = keys.iterator();
+ HashMap<String, Integer> expectedIntegers = new HashMap<>();
+ HashMap<String, String> expectedStrings = new HashMap<>();
+ HashMap<String, Boolean> expectedBooleans = new HashMap<>();
+ HashMap<String, Parcelable> expectedParcelables = new HashMap<>();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Object value = expectedBundle.get(key);
+
+ if (value instanceof Boolean) {
+ expectedBooleans.put(key, (Boolean) value);
+ } else if (value instanceof Integer) {
+ expectedIntegers.put(key, (Integer) value);
+ } else if (value instanceof String) {
+ expectedStrings.put(key, (String) value);
+ } else if (value instanceof Parcelable) {
+ expectedParcelables.put(key, (Parcelable) value);
+ }
+ }
+ assertThat("expected bundle: ", bundle, new BundleMatcher(expectedIntegers, expectedStrings, expectedBooleans, expectedParcelables));
+ assertEquals("expected resultCode: ", expectedResultCode, resultCode);
+ return null;
+ }
+ }).when(resultReceiver).send(anyInt(), any(Bundle.class));
+ return resultReceiver;
+ }
+
+ public static void mockFingerprintForCertificate(String mockedFingerprint) throws CertificateEncodingException, NoSuchAlgorithmException {
+ mockStatic(ConfigHelper.class);
+ when(ConfigHelper.getFingerprintFromCertificate(any(X509Certificate.class), anyString())).thenReturn(mockedFingerprint);
+ when(ConfigHelper.checkErroneousDownload(anyString())).thenCallRealMethod();
+ when(ConfigHelper.base64toHex(anyString())).thenCallRealMethod();
+ when(ConfigHelper.parseX509CertificateFromString(anyString())).thenCallRealMethod();
+ }
+
+ public static void mockProviderApiConnector(final BackendAnswerFabric.TestBackendErrorCase errorCase) throws IOException {
+ mockStatic(ProviderApiConnector.class);
+ when(ProviderApiConnector.canConnect(any(OkHttpClient.class), anyString())).thenReturn(errorCase != ERROR_NO_CONNECTION);
+ when(ProviderApiConnector.requestStringFromServer(anyString(), anyString(), nullable(String.class), ArgumentMatchers.<Pair<String,String>>anyList(), any(OkHttpClient.class))).thenAnswer(getAnswerForErrorcase(errorCase));
+ }
+
+ public static OkHttpClientGenerator mockClientGenerator() {
+ OkHttpClientGenerator mockClientGenerator = mock(OkHttpClientGenerator.class);
+ OkHttpClient mockedOkHttpClient = mock(OkHttpClient.class);
+ when(mockClientGenerator.initCommercialCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient);
+ when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class))).thenReturn(mockedOkHttpClient);
+ when(mockClientGenerator.initSelfSignedCAHttpClient(any(JSONObject.class), anyString())).thenReturn(mockedOkHttpClient);
+ return mockClientGenerator;
+ }
+
+ public static Resources mockResources(InputStream inputStream) throws IOException, JSONException {
+ Resources mockedResources = mock(Resources.class, RETURNS_DEEP_STUBS);
+ JSONObject errorMessages = new JSONObject(getInputAsString(inputStream));
+
+
+ when(mockedResources.getString(R.string.warning_corrupted_provider_details)).
+ thenReturn(errorMessages.getString("warning_corrupted_provider_details"));
+ when(mockedResources.getString(R.string.server_unreachable_message)).
+ thenReturn(errorMessages.getString("server_unreachable_message"));
+ when(mockedResources.getString(R.string.error_security_pinnedcertificate)).
+ thenReturn(errorMessages.getString("error.security.pinnedcertificate"));
+ when(mockedResources.getString(R.string.malformed_url)).
+ thenReturn(errorMessages.getString("malformed_url"));
+ when(mockedResources.getString(R.string.certificate_error)).
+ thenReturn(errorMessages.getString("certificate_error"));
+ when(mockedResources.getString(R.string.error_srp_math_error_user_message)).
+ thenReturn(errorMessages.getString("error_srp_math_error_user_message"));
+ when(mockedResources.getString(R.string.error_bad_user_password_user_message)).
+ thenReturn(errorMessages.getString("error_bad_user_password_user_message"));
+ when(mockedResources.getString(R.string.error_not_valid_password_user_message)).
+ thenReturn(errorMessages.getString("error_not_valid_password_user_message"));
+ when(mockedResources.getString(R.string.error_client_http_user_message)).
+ thenReturn(errorMessages.getString("error_client_http_user_message"));
+ when(mockedResources.getString(R.string.error_io_exception_user_message)).
+ thenReturn(errorMessages.getString("error_io_exception_user_message"));
+ when(mockedResources.getString(R.string.error_json_exception_user_message)).
+ thenReturn(errorMessages.getString("error_json_exception_user_message"));
+ when(mockedResources.getString(R.string.error_no_such_algorithm_exception_user_message)).
+ thenReturn(errorMessages.getString("error_no_such_algorithm_exception_user_message"));
+ when(mockedResources.getString(R.string.warning_corrupted_provider_details)).
+ thenReturn(errorMessages.getString("warning_corrupted_provider_details"));
+ when(mockedResources.getString(R.string.warning_corrupted_provider_cert)).
+ thenReturn(errorMessages.getString("warning_corrupted_provider_cert"));
+ when(mockedResources.getString(R.string.warning_expired_provider_cert)).
+ thenReturn(errorMessages.getString("warning_expired_provider_cert"));
+ return mockedResources;
+ }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java
new file mode 100644
index 00000000..00e276f4
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/BackendAnswerFabric.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils.answers;
+
+import org.mockito.stubbing.Answer;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class BackendAnswerFabric {
+ /**
+ * This enum can be useful to provide different responses from a mocked ProviderApiConnector
+ * in order to test different error scenarios
+ */
+ public enum TestBackendErrorCase {
+ NO_ERROR,
+ ERROR_NO_RESPONSE_BODY, // => NullPointerException
+ ERROR_DNS_RESOLUTION_ERROR, // => UnkownHostException
+ ERROR_SOCKET_TIMEOUT, // => SocketTimeoutException
+ ERROR_WRONG_PROTOCOL, // => MalformedURLException
+ ERROR_CERTIFICATE_INVALID, // => SSLHandshakeException
+ ERROR_WRONG_PORT, // => ConnectException
+ ERROR_PAYLOAD_MISSING, // => IllegalArgumentException
+ ERROR_TLS_1_2_NOT_SUPPORTED, // => UnknownServiceException
+ ERROR_UNKNOWN_IO_EXCEPTION, // => IOException
+ ERROR_NO_ACCESS,
+ ERROR_INVALID_SESSION_TOKEN,
+ ERROR_NO_CONNECTION,
+ ERROR_WRONG_SRP_CREDENTIALS
+ }
+
+ public static Answer<String> getAnswerForErrorcase(TestBackendErrorCase errorCase) {
+ switch (errorCase) {
+ case NO_ERROR:
+ return new NoErrorAnswer();
+ case ERROR_NO_RESPONSE_BODY:
+ break;
+ case ERROR_DNS_RESOLUTION_ERROR:
+ break;
+ case ERROR_SOCKET_TIMEOUT:
+ break;
+ case ERROR_WRONG_PROTOCOL:
+ break;
+ case ERROR_CERTIFICATE_INVALID:
+ break;
+ case ERROR_WRONG_PORT:
+ break;
+ case ERROR_PAYLOAD_MISSING:
+ break;
+ case ERROR_TLS_1_2_NOT_SUPPORTED:
+ break;
+ case ERROR_UNKNOWN_IO_EXCEPTION:
+ break;
+ case ERROR_NO_ACCESS:
+ break;
+ case ERROR_INVALID_SESSION_TOKEN:
+ break;
+ case ERROR_NO_CONNECTION:
+ break;
+ case ERROR_WRONG_SRP_CREDENTIALS:
+ break;
+ }
+ return null;
+ }
+
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java
new file mode 100644
index 00000000..cbf9f6b8
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/answers/NoErrorAnswer.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient.testutils.answers;
+
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static se.leap.bitmaskclient.testutils.TestSetupHelper.getInputAsString;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class NoErrorAnswer implements Answer<String> {
+ @Override
+ public String answer(InvocationOnMock invocation) throws Throwable {
+ String url = (String) invocation.getArguments()[0];
+ String requestMethod = (String) invocation.getArguments()[1];
+ String jsonPayload = (String) invocation.getArguments()[2];
+
+ if (url.contains("/provider.json")) {
+ //download provider json
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.json"));
+ } else if (url.contains("/ca.crt")) {
+ //download provider ca cert
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.net.pem"));
+ } else if (url.contains("config/eip-service.json")) {
+ // download provider service json containing gateways, locations and openvpn settings
+ return getInputAsString(getClass().getClassLoader().getResourceAsStream("riseup.service.json"));
+ } else if (url.contains("/users.json")) {
+ //create new user
+ //TODO: implement me
+ } else if (url.contains("/sessions.json")) {
+ //srp auth: sendAToSRPServer
+ //TODO: implement me
+ } else if (url.contains("/sessions/parmegvtest10.json")){
+ //srp auth: sendM1ToSRPServer
+ //TODO: implement me
+ }
+
+ return null;
+ }
+}
diff --git a/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
new file mode 100644
index 00000000..a7867e08
--- /dev/null
+++ b/app/src/test/java/se/leap/bitmaskclient/testutils/matchers/BundleMatcher.java
@@ -0,0 +1,192 @@
+package se.leap.bitmaskclient.testutils.matchers;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+
+/**
+ * Created by cyberta on 09.01.18.
+ */
+
+public class BundleMatcher extends BaseMatcher<Bundle> {
+
+ private final HashMap<String, Integer> expectedIntegers;
+ private final HashMap<String, String> expectedStrings;
+ private final HashMap<String, Boolean> expectedBooleans;
+ private final HashMap<String, Parcelable> expectedParcelables;
+ private HashMap<String, Integer> unfoundExpectedInteger = new HashMap<>();
+ private HashMap<String, Boolean> unfoundExpectedBoolean = new HashMap<>();
+ private HashMap<String, String> unfoundExpectedString = new HashMap<>();
+ private HashMap<String, Parcelable> unfoundExpectedParcelable = new HashMap<>();
+ private HashMap<String, Object> unexpectedAdditionalObjects = new HashMap<>();
+
+ public BundleMatcher(HashMap<String, Integer> expectedIntegers, HashMap<String, String> expectedStrings, HashMap<String, Boolean> expectedBooleans, HashMap<String, Parcelable> expectedParcelables) {
+ this.expectedBooleans = expectedBooleans;
+ this.expectedIntegers = expectedIntegers;
+ this.expectedStrings = expectedStrings;
+ this.expectedParcelables = expectedParcelables;
+ }
+
+ @Override
+ public boolean matches(Object item) {
+ if (item instanceof Bundle) {
+ Bundle actualBundle = (Bundle) item;
+ return checkActualBundleHasAllExpectedBooleanValues(actualBundle) &&
+ checkActualBundleHasAllExpectedStringValues(actualBundle) &&
+ checkActualBundleHasAllExpectedIntValues(actualBundle) &&
+ checkActualBundleHasAllExpectedParcelableValues(actualBundle) &&
+ checkUnexpectedAdditionalValuesIn(actualBundle);
+ }
+ return false;
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText("Bundle didn't match expectation!");
+
+ if (!unfoundExpectedInteger.isEmpty()) {
+ Iterator<String> iterator = unfoundExpectedInteger.keySet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ if (unfoundExpectedInteger.get(key) == null) {
+ description.appendText("\n unfound Integer in actual Bundle: ").appendValue(iterator.next());
+ } else {
+ description.appendText("\n expected Integer for key " + key).appendValue(expectedIntegers.get(key)).
+ appendText("\n found Integer was: ").appendValue(unfoundExpectedInteger.get(key));
+ }
+ }
+ }
+ if (!unfoundExpectedBoolean.isEmpty()) {
+ Iterator<String> iterator = unfoundExpectedBoolean.keySet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ if (unfoundExpectedBoolean.get(key) == null) {
+ description.appendText("\n unfound Boolean in actual Bundle: ").appendValue(iterator.next());
+ } else {
+ description.appendText("\n expected Boolean for key " + key).appendValue(expectedBooleans.get(key)).
+ appendText("\n found Boolean was: ").appendValue(unfoundExpectedBoolean.get(key));
+ }
+ }
+ }
+ if (!unfoundExpectedString.isEmpty()) {
+ Iterator<String> iterator = unfoundExpectedString.keySet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ if (unfoundExpectedString.get(key) == null) {
+ description.appendText("\n unfound String in actual Bundle: ").appendValue(iterator.next());
+ } else {
+ description.appendText("\n expected String for key " + key).appendValue(expectedStrings.get(key)).
+ appendText("\n found String was: ").appendValue(unfoundExpectedString.get(key));
+ }
+ }
+ }
+ if (!unfoundExpectedParcelable.isEmpty()) {
+ Iterator<String> iterator = unfoundExpectedInteger.keySet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ if (unfoundExpectedParcelable.get(key) == null) {
+ description.appendText("\n unfound Parcelable in actual Bundle: ").appendValue(iterator.next());
+ } else {
+ description.appendText("\n expected Parcelable or key " + key).appendValue(expectedParcelables.get(key)).
+ appendText("\n found Parcelable was: ").appendValue(unfoundExpectedParcelable.get(key));
+ }
+ }
+ }
+
+ if (!unexpectedAdditionalObjects.isEmpty()) {
+ Iterator<String> iterator = unexpectedAdditionalObjects.keySet().iterator();
+ while (iterator.hasNext()) {
+ String key = iterator.next();
+ Object value = unexpectedAdditionalObjects.get(key);
+ if (value instanceof String) {
+ description.appendText("\n unexpected String found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+ } else if (value instanceof Boolean) {
+ description.appendText("\n unexpected Boolean found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+ } else if (value instanceof Integer) {
+ description.appendText("\n unexpected Integer found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+ } else if (value instanceof Parcelable) {
+ description.appendText("\n unexpected Parcelable found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+ } else {
+ description.appendText("\n unexpected Object found in actual Bundle: ").appendValue(key).appendText(", ").appendValue(value);
+ }
+ }
+ }
+ }
+
+ private boolean checkActualBundleHasAllExpectedBooleanValues(Bundle actualBundle) {
+ Set<String> booleanKeys = expectedBooleans.keySet();
+ for (String key : booleanKeys) {
+ Object valueObject = actualBundle.get(key);
+ if (valueObject == null ||
+ !(valueObject instanceof Boolean) ||
+ valueObject != expectedBooleans.get(key)) {
+ unfoundExpectedBoolean.put(key, (Boolean) valueObject);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean checkActualBundleHasAllExpectedStringValues(Bundle actualBundle) {
+ Set<String> stringKeys = expectedStrings.keySet();
+ for (String key : stringKeys) {
+ Object valueObject = actualBundle.get(key);
+ if (valueObject == null ||
+ !(valueObject instanceof String) ||
+ !valueObject.equals(expectedStrings.get(key))) {
+ unfoundExpectedString.put(key, (String) valueObject);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean checkActualBundleHasAllExpectedIntValues(Bundle actualBundle) {
+ Set<String> stringKeys = expectedIntegers.keySet();
+ for (String key : stringKeys) {
+ Object valueObject = actualBundle.get(key);
+ if (valueObject == null ||
+ !(valueObject instanceof Integer) ||
+ ((Integer) valueObject).compareTo(expectedIntegers.get(key)) != 0) {
+ unfoundExpectedInteger.put(key, (Integer) valueObject);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean checkActualBundleHasAllExpectedParcelableValues(Bundle actualBundle) {
+ Set<String> stringKeys = expectedParcelables.keySet();
+ for (String key : stringKeys) {
+ Object valueObject = actualBundle.get(key);
+ if (valueObject == null ||
+ !(valueObject instanceof Parcelable) ||
+ !valueObject.equals(expectedParcelables.get(key))) {
+ unfoundExpectedParcelable.put(key, (Parcelable) valueObject);
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private boolean checkUnexpectedAdditionalValuesIn(Bundle actualBundle) {
+ Set<String> keys = actualBundle.keySet();
+
+ for (String key : keys) {
+ if (!expectedStrings.containsKey(key) &&
+ !expectedIntegers.containsKey(key) &&
+ !expectedBooleans.containsKey(key) &&
+ !expectedParcelables.containsKey(key)
+ ) {
+ unexpectedAdditionalObjects.put(key, actualBundle.getString(key));
+ }
+ }
+ return unexpectedAdditionalObjects.isEmpty();
+ }
+}
diff --git a/app/src/test/resources/error_messages.json b/app/src/test/resources/error_messages.json
new file mode 100644
index 00000000..486a3dab
--- /dev/null
+++ b/app/src/test/resources/error_messages.json
@@ -0,0 +1,16 @@
+{
+ "server_unreachable_message": "Server is unreachable, please try again.",
+"error.security.pinnedcertificate": "Security error, update the app or choose another provider.",
+"malformed_url": "It doesn't seem to be a Bitmask provider.",
+"certificate_error": "This is not a trusted Bitmask provider.",
+"error_srp_math_error_user_message": "Try again: server math error.",
+"error_bad_user_password_user_message": "Incorrect username or password.",
+"error_not_valid_password_user_message": "It should have at least 8 characters.",
+"error_client_http_user_message": "Try again: Client HTTP error",
+"error_io_exception_user_message": "Try again: I/O error",
+"error_json_exception_user_message": "Try again: Bad response from the server",
+"error_no_such_algorithm_exception_user_message": "Encryption algorithm not found. Please update your OS!",
+"warning_corrupted_provider_details": "Stored provider details are corrupted. You can either update Bitmask (recommended) or update the provider details using a commercial CA certificate.",
+"warning_corrupted_provider_cert": "Stored provider certificate is corrupted. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.",
+"warning_expired_provider_cert": "Stored provider certificate is expired. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate."
+} \ No newline at end of file
diff --git a/app/src/test/resources/riseup.net.json b/app/src/test/resources/riseup.net.json
new file mode 100644
index 00000000..9a5ec79e
--- /dev/null
+++ b/app/src/test/resources/riseup.net.json
@@ -0,0 +1,37 @@
+{
+ "api_uri": "https://api.black.riseup.net:443",
+ "api_version": "1",
+ "ca_cert_fingerprint": "SHA256: a5244308a1374709a9afce95e3ae47c1b44bc2398c0a70ccbf8b3a8a97f29494",
+ "ca_cert_uri": "https://black.riseup.net/ca.crt",
+ "default_language": "en",
+ "description": {
+ "en": "Riseup is a non-profit collective in Seattle that provides online communication tools for people and groups working toward liberatory social change."
+ },
+ "domain": "riseup.net",
+ "enrollment_policy": "open",
+ "languages": [
+ "en"
+ ],
+ "name": {
+ "en": "Riseup Networks"
+ },
+ "service": {
+ "allow_anonymous": false,
+ "allow_free": true,
+ "allow_limited_bandwidth": false,
+ "allow_paid": false,
+ "allow_registration": true,
+ "allow_unlimited_bandwidth": true,
+ "bandwidth_limit": 102400,
+ "default_service_level": 1,
+ "levels": {
+ "1": {
+ "description": "Please donate.",
+ "name": "free"
+ }
+ }
+ },
+ "services": [
+ "openvpn"
+ ]
+} \ No newline at end of file
diff --git a/app/src/test/resources/riseup.net.pem b/app/src/test/resources/riseup.net.pem
new file mode 100644
index 00000000..c890aff4
--- /dev/null
+++ b/app/src/test/resources/riseup.net.pem
@@ -0,0 +1,32 @@
+-----BEGIN CERTIFICATE-----
+MIIFjTCCA3WgAwIBAgIBATANBgkqhkiG9w0BAQ0FADBZMRgwFgYDVQQKDA9SaXNl
+dXAgTmV0d29ya3MxGzAZBgNVBAsMEmh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UE
+AwwXUmlzZXVwIE5ldHdvcmtzIFJvb3QgQ0EwHhcNMTQwNDI4MDAwMDAwWhcNMjQw
+NDI4MDAwMDAwWjBZMRgwFgYDVQQKDA9SaXNldXAgTmV0d29ya3MxGzAZBgNVBAsM
+Emh0dHBzOi8vcmlzZXVwLm5ldDEgMB4GA1UEAwwXUmlzZXVwIE5ldHdvcmtzIFJv
+b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC76J4ciMJ8Sg0m
+TP7DF2DT9zNe0Csk4myoMFC57rfJeqsAlJCv1XMzBmXrw8wq/9z7XHv6n/0sWU7a
+7cF2hLR33ktjwODlx7vorU39/lXLndo492ZBhXQtG1INMShyv+nlmzO6GT7ESfNE
+LliFitEzwIegpMqxCIHXFuobGSCWF4N0qLHkq/SYUMoOJ96O3hmPSl1kFDRMtWXY
+iw1SEKjUvpyDJpVs3NGxeLCaA7bAWhDY5s5Yb2fA1o8ICAqhowurowJpW7n5ZuLK
+5VNTlNy6nZpkjt1QycYvNycffyPOFm/Q/RKDlvnorJIrihPkyniV3YY5cGgP+Qkx
+HUOT0uLA6LHtzfiyaOqkXwc4b0ZcQD5Vbf6Prd20Ppt6ei0zazkUPwxld3hgyw58
+m/4UIjG3PInWTNf293GngK2Bnz8Qx9e/6TueMSAn/3JBLem56E0WtmbLVjvko+LF
+PM5xA+m0BmuSJtrD1MUCXMhqYTtiOvgLBlUm5zkNxALzG+cXB28k6XikXt6MRG7q
+hzIPG38zwkooM55yy5i1YfcIi5NjMH6A+t4IJxxwb67MSb6UFOwg5kFokdONZcwj
+shczHdG9gLKSBIvrKa03Nd3W2dF9hMbRu//STcQxOailDBQCnXXfAATj9pYzdY4k
+ha8VCAREGAKTDAex9oXf1yRuktES4QIDAQABo2AwXjAdBgNVHQ4EFgQUC4tdmLVu
+f9hwfK4AGliaet5KkcgwDgYDVR0PAQH/BAQDAgIEMAwGA1UdEwQFMAMBAf8wHwYD
+VR0jBBgwFoAUC4tdmLVuf9hwfK4AGliaet5KkcgwDQYJKoZIhvcNAQENBQADggIB
+AGzL+GRnYu99zFoy0bXJKOGCF5XUXP/3gIXPRDqQf5g7Cu/jYMID9dB3No4Zmf7v
+qHjiSXiS8jx1j/6/Luk6PpFbT7QYm4QLs1f4BlfZOti2KE8r7KRDPIecUsUXW6P/
+3GJAVYH/+7OjA39za9AieM7+H5BELGccGrM5wfl7JeEz8in+V2ZWDzHQO4hMkiTQ
+4ZckuaL201F68YpiItBNnJ9N5nHr1MRiGyApHmLXY/wvlrOpclh95qn+lG6/2jk7
+3AmihLOKYMlPwPakJg4PYczm3icFLgTpjV5sq2md9bRyAg3oPGfAuWHmKj2Ikqch
+Td5CHKGxEEWbGUWEMP0s1A/JHWiCbDigc4Cfxhy56CWG4q0tYtnc2GMw8OAUO6Wf
+Xu5pYKNkzKSEtT/MrNJt44tTZWbKV/Pi/N2Fx36my7TgTUj7g3xcE9eF4JV2H/sg
+tsK3pwE0FEqGnT4qMFbixQmc8bGyuakr23wjMvfO7eZUxBuWYR2SkcP26sozF9PF
+tGhbZHQVGZUTVPyvwahMUEhbPGVerOW0IYpxkm0x/eaWdTc4vPpf/rIlgbAjarnJ
+UN9SaWRlWKSdP4haujnzCoJbM7dU9bjvlGZNyXEekgeT0W2qFeGGp+yyUWw8tNsp
+0BuC1b7uW/bBn/xKm319wXVDvBgZgcktMolak39V7DVO
+-----END CERTIFICATE----- \ No newline at end of file
diff --git a/app/src/test/resources/riseup.service.json b/app/src/test/resources/riseup.service.json
new file mode 100644
index 00000000..05f23bc1
--- /dev/null
+++ b/app/src/test/resources/riseup.service.json
@@ -0,0 +1,93 @@
+{
+ "gateways":[
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "ports":[
+ "443"
+ ],
+ "protocols":[
+ "tcp"
+ ],
+ "transport":[
+ "openvpn"
+ ],
+ "user_ips":false
+ },
+ "host":"garza.riseup.net",
+ "ip_address":"198.252.153.28",
+ "location":"seattle"
+ },
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "ports":[
+ "443"
+ ],
+ "protocols":[
+ "tcp"
+ ],
+ "transport":[
+ "openvpn"
+ ],
+ "user_ips":false
+ },
+ "host":"tenca.riseup.net",
+ "ip_address":"5.79.86.180",
+ "location":"amsterdam"
+ },
+ {
+ "capabilities":{
+ "adblock":false,
+ "filter_dns":false,
+ "limited":false,
+ "ports":[
+ "443"
+ ],
+ "protocols":[
+ "tcp"
+ ],
+ "transport":[
+ "openvpn"
+ ],
+ "user_ips":false
+ },
+ "host":"yal.riseup.net",
+ "ip_address":"199.58.81.145",
+ "location":"montreal"
+ }
+ ],
+ "locations":{
+ "amsterdam":{
+ "country_code":"NL",
+ "hemisphere":"N",
+ "name":"Amsterdam",
+ "timezone":"+2"
+ },
+ "montreal":{
+ "country_code":"CA",
+ "hemisphere":"N",
+ "name":"Montreal",
+ "timezone":"-5"
+ },
+ "seattle":{
+ "country_code":"US",
+ "hemisphere":"N",
+ "name":"Seattle",
+ "timezone":"-7"
+ }
+ },
+ "openvpn_configuration":{
+ "auth":"SHA1",
+ "cipher":"AES-128-CBC",
+ "keepalive":"10 30",
+ "tls-cipher":"DHE-RSA-AES128-SHA",
+ "tun-ipv6":true
+ },
+ "serial":1,
+ "version":1
+} \ No newline at end of file
diff --git a/app/src/test/resources/secrets.json b/app/src/test/resources/secrets.json
index 36ba5977..f3b68919 100644
--- a/app/src/test/resources/secrets.json
+++ b/app/src/test/resources/secrets.json
@@ -1 +1 @@
-{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"}
+{"ca_cert":"-----BEGIN CERTIFICATE-----\nMIIFbzCCA1egAwIBAgIBATANBgkqhkiG9w0BAQ0FADBKMRgwFgYDVQQDDA9CaXRt\nYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNVBAsME2h0dHBzOi8v\nYml0bWFzay5uZXQwHhcNMTIxMTA2MDAwMDAwWhcNMjIxMTA2MDAwMDAwWjBKMRgw\nFgYDVQQDDA9CaXRtYXNrIFJvb3QgQ0ExEDAOBgNVBAoMB0JpdG1hc2sxHDAaBgNV\nBAsME2h0dHBzOi8vYml0bWFzay5uZXQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw\nggIKAoICAQC1eV4YvayaU+maJbWrD4OHo3d7S1BtDlcvkIRS1Fw3iYDjsyDkZxai\ndHp4EUasfNQ+EVtXUvtk6170EmLco6Elg8SJBQ27trE6nielPRPCfX3fQzETRfvB\n7tNvGw4Jn2YKiYoMD79kkjgyZjkJ2r\/bEHUSevmR09BRp86syHZerdNGpXYhcQ84\nCA1+V+603GFIHnrP+uQDdssW93rgDNYu+exT+Wj6STfnUkugyjmPRPjL7wh0tzy+\nznCeLl4xiV3g9sjPnc7r2EQKd5uaTe3j71sDPF92KRk0SSUndREz+B1+Dbe\/RGk4\nMEqGFuOzrtsgEhPIX0hplhb0Tgz\/rtug+yTT7oJjBa3u20AAOQ38\/M99EfdeJvc4\nlPFF1XBBLh6X9UKF72an2NuANiX6XPySnJgZ7nZ09RiYZqVwu\/qt3DfvLfhboq+0\nbQvLUPXrVDr70onv5UDjpmEA\/cLmaIqqrduuTkFZOym65\/PfAPvpGnt7crQj\/Ibl\nDEDYZQmP7AS+6zBjoOzNjUGE5r40zWAR1RSi7zliXTu+yfsjXUIhUAWmYR6J3KxB\nlfsiHBQ+8dn9kC3YrUexWoOqBiqJOAJzZh5Y1tqgzfh+2nmHSB2dsQRs7rDRRlyy\nYMbkpzL9ZsOUO2eTP1mmar6YjCN+rggYjRrX71K2SpBG6b1zZxOG+wIDAQABo2Aw\nXjAdBgNVHQ4EFgQUuYGDLL2sswnYpHHvProt1JU+D48wDgYDVR0PAQH\/BAQDAgIE\nMAwGA1UdEwQFMAMBAf8wHwYDVR0jBBgwFoAUuYGDLL2sswnYpHHvProt1JU+D48w\nDQYJKoZIhvcNAQENBQADggIBADeG67vaFcbITGpi51264kHPYPEWaXUa5XYbtmBl\ncXYyB6hY5hv\/YNuVGJ1gWsDmdeXEyj0j2icGQjYdHRfwhrbEri+h1EZOm1cSBDuY\nk\/P5+ctHyOXx8IE79DBsZ6IL61UKIaKhqZBfLGYcWu17DVV6+LT+AKtHhOrv3TSj\nRnAcKnCbKqXLhUPXpK0eTjPYS2zQGQGIhIy9sQXVXJJJsGrPgMxna1Xw2JikBOCG\nhtD\/JKwt6xBmNwktH0GI\/LVtVgSp82Clbn9C4eZN9E5YbVYjLkIEDhpByeC71QhX\nEIQ0ZR56bFuJA\/CwValBqV\/G9gscTPQqd+iETp8yrFpAVHOW+YzSFbxjTEkBte1J\naF0vmbqdMAWLk+LEFPQRptZh0B88igtx6tV5oVd+p5IVRM49poLhuPNJGPvMj99l\nmlZ4+AeRUnbOOeAEuvpLJbel4rhwFzmUiGoeTVoPZyMevWcVFq6BMkS+jRR2w0jK\nG6b0v5XDHlcFYPOgUrtsOBFJVwbutLvxdk6q37kIFnWCd8L3kmES5q4wjyFK47Co\nJa8zlx64jmMZPg\/t3wWqkZgXZ14qnbyG5\/lGsj5CwVtfDljrhN0oCWK1FZaUmW3d\n69db12\/g4f6phldhxiWuGC\/W6fCW5kre7nmhshcltqAJJuU47iX+DarBFiIj816e\nyV8e\n-----END CERTIFICATE-----\n","cert":"-----BEGIN CERTIFICATE-----\nMIIEjDCCAnSgAwIBAgIQG6MBp\/cd9DlY+7cdvp3R3jANBgkqhkiG9w0BAQsFADBmMRAwDgYDVQQK\nDAdCaXRtYXNrMRwwGgYDVQQLDBNodHRwczovL2JpdG1hc2submV0MTQwMgYDVQQDDCtCaXRtYXNr\nIFJvb3QgQ0EgKGNsaWVudCBjZXJ0aWZpY2F0ZXMgb25seSEpMB4XDTE0MTIwNTAwMDAwMFoXDTE1\nMDMwNTAwMDAwMFowLTErMCkGA1UEAwwiVU5MSU1JVEVEZDBwZDdkMzE4eTNtOHNkeXllaTFqYmZl\neDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANRNhZ4aCwdL5+OKObOKeI2rDqEwGnIr\nhL9wzo\/FXbwLfdW45Y9Mxwhh6xy2NkA1YUKCB8VNBKNXlBrGr1QriLbu1rItsJ2VVLqGluVV\/gO4\njcaPU+\/Wu0hMFKG28J\/dPvIGeNbjBWk6mxQAA5WIpRK9RTeQ88wVaGIZDDzIdivza2zpcyiPAyii\ndbkyXh7sLsKvbZB6wLrert6Y1ylR3SlkZP0LfdGAMAdkMyuXKOjgcSnUltR8HSBuZcSUlsTVM11n\nrYeGCYyPNNQ3UYatDW33UASgRDBorrmjhhKP7IW\/opdlnPk5ZrP3i0qI32\/boRe0EWZGXJvr4P3K\ndJ30uCECAwEAAaNvMG0wHQYDVR0OBBYEFK8bMVAM4GBB5sHptoIOAaIvlYueMAsGA1UdDwQEAwIH\ngDATBgNVHSUEDDAKBggrBgEFBQcDAjAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFId+E7bsWFsUWah9\nvZuPvZ7O+aJsMA0GCSqGSIb3DQEBCwUAA4ICAQAQOX81csVhvP422NKkZH7+g3npBpl+sEHedaGR\nxYPOu4HrA4TVF9h44sljRoRJyenGNdBZCXcLKHg889eePTf8Z5K3lTojp6hvwyA6tgxOMHT1kESW\nPfqnRw8mHfHJuE3g+4YNUMwggzwc\/VZATdV\/7M33sarVN9AUOHou9n9BizgCC+UnYlS+F2POumE3\nFbOhKo5uubI02MwBYlN2JVO2TBt1Q20w8wc6cU07Xi5Epp+1mkgFiOShkNtPcJmEyBWJhxDtSDOW\n2doqWYNqH2kq7B5R\/kyyfcpFJqAnBTV7xs+C5rTS1mW7LpxfdCUMbYuLCpyxpO3A\/DhAm8n47tUH\nlBtmo8Avdb8VdFpYiGBpB0o9kTFcsWFb2GkWFBduGfSEB8jUI7QtqhgZqocAKK\/cweSRV8FwyUcn\nR0prRm3QEi9fbXqEddzjSY9y\/lqWYzT7u+IOAQpKroeZ4wzgYperDNOUFuYk1rP7yuvjP2pV5rcN\nyPoBP60TPVWMRM4WJm6nTogAz2qBrFsf\/XwT\/ajzbsjT6HNB7QbRE+wkFkqspoXG5Agp7KQ8lW3L\nSKCDGOQJz7VIE85pD0tg7QEXBEw8oaRZtMjQ0Gvs25mxXAKka4wGasaWfYH6d0E+iKYcWn86V1rH\nK2ZoknT+Nno5jgjFuUR3fZseNizEfx7BteooKQ==\n-----END CERTIFICATE-----","Constants.PROVIDER_PRIVATE_KEY":"-----BEGIN RSA PRIVATE KEY-----\nMIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQDUTYWeGgsHS+fjijmziniNqw6h\nMBpyK4S\/cM6PxV28C33VuOWPTMcIYesctjZANWFCggfFTQSjV5Qaxq9UK4i27tayLbCdlVS6hpbl\nVf4DuI3Gj1Pv1rtITBShtvCf3T7yBnjW4wVpOpsUAAOViKUSvUU3kPPMFWhiGQw8yHYr82ts6XMo\njwMoonW5Ml4e7C7Cr22QesC63q7emNcpUd0pZGT9C33RgDAHZDMrlyjo4HEp1JbUfB0gbmXElJbE\n1TNdZ62HhgmMjzTUN1GGrQ1t91AEoEQwaK65o4YSj+yFv6KXZZz5OWaz94tKiN9v26EXtBFmRlyb\n6+D9ynSd9LghAgMBAAECggEBANPHLRXkhsHVj1EkzqBx7gXr8CEMmiTvknFh9zvltrZhhDoRQjWr\nchPDkcRHY2Cznvy4N0YyqQDD2ULIlZdSAgPxxothFoBruWSD47yMBmLx08ORsDpcqt\/YvPAATJI8\nIpFNsXcyaXBp\/M57oRemgnxp\/8UJPJmFdWX99H4hvffh\/jdj7POgYiWUaAl37XTYZKZ4nzKU2wpL\nEDLj9RKPz9gG7CYp2zrLC9LaAsrXVrKwPBw6g+XwbClaqFj97db3mrY4lr6mTo89qmus1AU+fBDH\n3Xlpmc8JwB+30TvhRNKrpLx9cEjuEj7K1gm8Y4dWCjPi+lNbtAyUBcgPJFa\/81ECgYEA7pLoBU\/Y\nZYjyHFca8FvDBcBh6haHfqJr9doXWtgjDrbi3o2n5wHqfKhFWOH6vPEQozkOVeX1ze6HOiRmGBpW\nr+r7x8TD25L7I6HJw3M351RWOAfkF0w\/RTVdetcTgduQtfN1u6BDhYSVceXMjyQYx7MhfETWI8Gh\nKSYm8OEDYiUCgYEA489fmbrCcUnXzpTsbswJ5NmSoEXbcX8cLxnQuzE0z9GHhQdrMjOpXR76reTW\n6jcuudarNcwRUYSWWhjCDKHhpx4HhasWPaHgr7jIzcRw8yZSJRSxKr8sl1qh6g7s47JcmfXOMWLt\nyuyE933XrT19Th4ODZHY40Uv35mPjMi9d00CgYEAyRNAQtndBRa7GG\/B4Ls2T+6pl+aNJIo4e+no\nrURlp800wWabEPRocdBRQmyULBLxduBr2LIMzhgwGSz8b2wji\/l9ZA3PFY135bxClVzSzUIjuO3N\nrGUzHl2wAAyuAFDSUshzfkPBJRNt8aVBF5PQ3t93ZYmPAmv8LPZe875yX5ECgYEAsUEcwK\/ZNW7g\ndQPZR4iJNkC4Xu6cBZ6Cnn92swBheEYvLSoNlX0vDZ7aLE3\/jzQqrjzC8NP8sbH5jtbuvgeDXZX3\nAmGRp5j6C6A61ihAPmEVz3ZfN8SSfJ3vl\/\/PAIg6lyz0J+cy4Q7RkwSeuVQ72Hl4M8TEvmmKC3Af\nispy6Y0CgYEAgl1o2lo+ACyk+oVQPaaPqK3d7WOBFp4eR2nXFor\/vsx9igQOlZUgzRDQsR8jo1o9\nefOSBf87igrZGgssys89pWa2dnXnz5PMmzkKr6bw4D9Ez6u6Puc9UZhGw\/8wDYg6fSosdB9utspm\nM698ycef7jBNMDgmhpSvfw5GctoNQ4s=\n-----END RSA PRIVATE KEY-----"}