summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/AndroidManifest.xml24
-rw-r--r--app/src/main/assets/urls/bitmask demo.url3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java47
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java110
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Dashboard.java39
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java85
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/OkHttpClientGenerator.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java141
-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.java98
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java (renamed from app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java)576
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderManager.java71
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/StartActivity.java2
-rw-r--r--app/src/main/res/values/strings.xml5
16 files changed, 1122 insertions, 374 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 14ac77ef..2d42e922 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -26,11 +26,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
- <uses-permission
- android:name="android.permission.WRITE_EXTERNAL_STORAGE"
- android:maxSdkVersion="18" />
- <uses-permission android:name="android.permission.READ_PHONE_STATE" />
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+ android:maxSdkVersion="18"/>
<application
android:name=".BitmaskApp"
@@ -60,10 +57,10 @@
<receiver
android:name=".OnBootReceiver"
android:enabled="true"
- android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
- <intent-filter android:priority="999">
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
+ android:permission="android.permission.RECEIVE_BOOT_COMPLETED" >
+ <intent-filter android:priority="999">
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
</receiver>
<activity
@@ -82,7 +79,9 @@
android:label="@string/app_name"
android:launchMode="singleTop"
android:noHistory="true"
- android:theme="@style/SplashTheme">
+ android:theme="@style/SplashTheme"
+ >
+
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.MAIN" />
@@ -110,9 +109,6 @@
android:name=".eip.EIP"
android:exported="false">
<intent-filter>
- <action android:name="se.leap.bitmaskclient.eip.UPDATE_EIP_SERVICE" />
- <action android:name="se.leap.bitmaskclient.eip.START_EIP" />
- <action android:name="se.leap.bitmaskclient.eip.STOP_EIP" />
<action android:name="se.leap.bitmaskclient.EIP.UPDATE"/>
<action android:name="se.leap.bitmaskclient.EIP.START"/>
<action android:name="se.leap.bitmaskclient.EIP.STOP"/>
@@ -122,4 +118,4 @@
</service>
</application>
-</manifest> \ No newline at end of file
+</manifest>
diff --git a/app/src/main/assets/urls/bitmask demo.url b/app/src/main/assets/urls/bitmask demo.url
deleted file mode 100644
index 1a412055..00000000
--- a/app/src/main/assets/urls/bitmask demo.url
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "main_url" : "https://demo.bitmask.net/"
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
index 9253cfe6..0b51af27 100644
--- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
@@ -57,6 +57,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.ProviderAPI.ERRORS;
/**
* abstract base Activity that builds and shows the list of known available providers.
@@ -175,10 +176,10 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
// by the height of mProgressbar (and the height of the first list item)
mProgressBar.setVisibility(INVISIBLE);
progressbar_description.setVisibility(INVISIBLE);
-
+ mProgressBar.setProgress(0);
}
- private void showProgressBar() {
+ protected void showProgressBar() {
mProgressBar.setVisibility(VISIBLE);
progressbar_description.setVisibility(VISIBLE);
}
@@ -206,6 +207,8 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
String provider_json_string = preferences.getString(Provider.KEY, "");
if (!provider_json_string.isEmpty())
selected_provider.define(new JSONObject(provider_json_string));
+ String caCert = preferences.getString(Provider.CA_CERT, "");
+ selected_provider.setCACert(caCert);
} catch (JSONException e) {
e.printStackTrace();
}
@@ -222,12 +225,11 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
}
} else if (resultCode == ProviderAPI.PROVIDER_NOK) {
mConfigState.setAction(PROVIDER_NOT_SET);
- hideProgressBar();
preferences.edit().remove(Provider.KEY).apply();
setResult(RESULT_CANCELED, mConfigState);
- String reason_to_fail = resultData.getString(ProviderAPI.ERRORS);
+ String reason_to_fail = resultData.getString(ERRORS);
showDownloadFailedDialog(reason_to_fail);
} else if (resultCode == ProviderAPI.CORRECTLY_DOWNLOADED_CERTIFICATE) {
mProgressBar.incrementProgressBy(1);
@@ -284,12 +286,28 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
cancelSettingUpProvider();
}
+ @Override
public void cancelSettingUpProvider() {
+ hideProgressBar();
mConfigState.setAction(PROVIDER_NOT_SET);
adapter.showAllProviders();
preferences.edit().remove(Provider.KEY).remove(Constants.PROVIDER_ALLOW_ANONYMOUS).remove(Constants.PROVIDER_KEY).apply();
}
+ @Override
+ public void updateProviderDetails() {
+ mConfigState.setAction(SETTING_UP_PROVIDER);
+ Intent provider_API_command = new Intent(this, ProviderAPI.class);
+
+ provider_API_command.setAction(ProviderAPI.UPDATE_PROVIDER_DETAILS);
+ provider_API_command.putExtra(ProviderAPI.RECEIVER_KEY, providerAPI_result_receiver);
+ Bundle parameters = new Bundle();
+ parameters.putString(Provider.MAIN_URL, selected_provider.getMainUrl().toString());
+ provider_API_command.putExtra(ProviderAPI.PARAMETERS, parameters);
+
+ startService(provider_API_command);
+ }
+
private void askDashboardToQuitApp() {
Intent ask_quit = new Intent();
ask_quit.putExtra(Constants.APP_ACTION_QUIT, Constants.APP_ACTION_QUIT);
@@ -325,7 +343,7 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
}
/**
- * 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);
@@ -360,24 +378,28 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
/**
* Shows an error dialog, if configuring of a provider failed.
*
- * @param reason_to_fail
+ * @param reasonToFail
*/
- public void showDownloadFailedDialog(String reason_to_fail) {
+ public void showDownloadFailedDialog(String reasonToFail) {
try {
FragmentTransaction fragment_transaction = fragment_manager.removePreviousFragment(DownloadFailedDialog.TAG);
-
- DialogFragment newFragment = DownloadFailedDialog.newInstance(reason_to_fail);
+ DialogFragment newFragment;
+ try {
+ JSONObject errorJson = new JSONObject(reasonToFail);
+ newFragment = DownloadFailedDialog.newInstance(errorJson);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ newFragment = DownloadFailedDialog.newInstance(reasonToFail);
+ }
newFragment.show(fragment_transaction, DownloadFailedDialog.TAG);
} catch (IllegalStateException e) {
e.printStackTrace();
mConfigState.setAction(PENDING_SHOW_FAILED_DIALOG);
- mConfigState.putExtra(REASON_TO_FAIL, reason_to_fail);
+ mConfigState.putExtra(REASON_TO_FAIL, reasonToFail);
}
}
-
-
/**
* Once selected a provider, this fragment offers the user to log in,
* use it anonymously (if possible)
@@ -391,7 +413,6 @@ public abstract class BaseConfigurationWizard extends ButterKnifeActivity
startActivity(intent);
}
-
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.configuration_wizard_activity, menu);
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index fd1e2080..0e861059 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -16,24 +16,44 @@
*/
package se.leap.bitmaskclient;
-import android.util.*;
+import android.support.annotation.NonNull;
+import android.util.Log;
-import org.json.*;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.spongycastle.util.encoders.Base64;
-import java.io.*;
-import java.math.*;
-import java.security.*;
-import java.security.cert.*;
-import java.security.interfaces.*;
-import java.security.spec.*;
+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;
/**
- * Stores constants, and implements auxiliary methods used across all LEAP Android classes.
+ * Stores constants, and implements auxiliary methods used across all Bitmask Android classes.
*
* @author parmegv
* @author MeanderingCode
*/
public class ConfigHelper {
+ private static final String TAG = ConfigHelper.class.getName();
private static KeyStore keystore_trusted;
final public static String NG_1024 =
@@ -42,7 +62,7 @@ public class ConfigHelper {
public static boolean checkErroneousDownload(String downloaded_string) {
try {
- if (new JSONObject(downloaded_string).has(ProviderAPI.ERRORS) || downloaded_string.isEmpty()) {
+ if (downloaded_string == null || downloaded_string.isEmpty() || new JSONObject(downloaded_string).has(ProviderAPI.ERRORS)) {
return true;
} else {
return false;
@@ -79,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);
@@ -87,24 +107,50 @@ 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;
}
+
+ public static String loadInputStreamAsString(InputStream inputStream) {
+ BufferedReader in = null;
+ try {
+ StringBuilder buf = new StringBuilder();
+ in = new BufferedReader(new InputStreamReader(inputStream));
+
+ String str;
+ boolean isFirst = true;
+ while ( (str = in.readLine()) != null ) {
+ if (isFirst)
+ isFirst = false;
+ else
+ buf.append('\n');
+ buf.append(str);
+ }
+ return buf.toString();
+ } catch (IOException e) {
+ Log.e(TAG, "Error opening asset " + name);
+ } finally {
+ if (in != null) {
+ try {
+ in.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Error closing asset " + name);
+ }
+ }
+ }
+
+ return null;
+ }
+
protected static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
RSAPrivateKey key = null;
try {
KeyFactory kf = KeyFactory.getInstance("RSA", "BC");
rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString, Base64.DEFAULT));
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
key = (RSAPrivateKey) kf.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
// TODO Auto-generated catch block
@@ -126,6 +172,32 @@ public class ConfigHelper {
return key;
}
+ private static String byteArrayToHex(byte[] input) {
+ int readBytes = input.length;
+ StringBuffer hexData = new StringBuffer();
+ int onebyte;
+ for (int i = 0; i < readBytes; i++) {
+ onebyte = ((0x000000ff & input[i]) | 0xffffff00);
+ hexData.append(Integer.toHexString(onebyte).substring(6));
+ }
+ return hexData.toString();
+ }
+
+ /**
+ * Calculates the hexadecimal representation of a sha256/sha1 fingerprint of a certificate
+ *
+ * @param certificate
+ * @param encoding
+ * @return
+ * @throws NoSuchAlgorithmException
+ * @throws CertificateEncodingException
+ */
+ @NonNull
+ public static String getFingerprintFromCertificate(X509Certificate certificate, String encoding) throws NoSuchAlgorithmException, CertificateEncodingException /*, UnsupportedEncodingException*/ {
+ byte[] byteArray = MessageDigest.getInstance(encoding).digest(certificate.getEncoded());
+ return byteArrayToHex(byteArray);
+ }
+
/**
* Adds a new X509 certificate given its input stream and its provider name
*
diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
index b36c8cc2..25c1f64e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
@@ -25,7 +25,6 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentTransaction;
-import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -37,10 +36,13 @@ import org.json.JSONObject;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
-import butterknife.ButterKnife;
import butterknife.InjectView;
import se.leap.bitmaskclient.fragments.AboutFragment;
+import de.blinkt.openvpn.core.VpnStatus;
import se.leap.bitmaskclient.userstatus.SessionDialog;
import se.leap.bitmaskclient.userstatus.User;
import se.leap.bitmaskclient.userstatus.UserStatusFragment;
@@ -101,6 +103,11 @@ public class Dashboard extends ButterKnifeActivity {
handledVersion = true;
}
+ // initialize app necessities
+ ProviderAPICommand.initialize(this);
+ VpnStatus.initLogCache(getApplicationContext().getCacheDir());
+ User.init(getString(R.string.default_username));
+
prepareEIP(savedInstanceState);
}
@@ -144,6 +151,7 @@ public class Dashboard extends ButterKnifeActivity {
try {
provider.setUrl(new URL(preferences.getString(Provider.MAIN_URL, "")));
provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
+ provider.setCACert(preferences.getString(Provider.CA_CERT, ""));
} catch (MalformedURLException | JSONException e) {
e.printStackTrace();
}
@@ -243,10 +251,9 @@ public class Dashboard extends ButterKnifeActivity {
}
@SuppressLint("CommitPrefEdits")
private void providerToPreferences(Provider provider) {
- //FIXME: figure out why .commit() is used and try refactor that cause, currently runs on UI thread
- preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).commit();
- preferences.edit().putString(Provider.MAIN_URL, provider.mainUrl().toString()).apply();
- preferences.edit().putString(Provider.KEY, provider.definition().toString()).apply();
+ preferences.edit().putBoolean(Constants.PROVIDER_CONFIGURED, true).
+ putString(Provider.MAIN_URL, provider.getMainUrl().toString()).
+ putString(Provider.KEY, provider.getDefinition().toString()).apply();
}
private void configErrorDialog() {
@@ -398,7 +405,25 @@ public class Dashboard extends ButterKnifeActivity {
private void switchProvider() {
if (provider.hasEIP()) eip_fragment.stopEipIfPossible();
- preferences.edit().clear().apply();
+ Map<String, ?> allEntries = preferences.getAll();
+ List<String> lastProvidersKeys = new ArrayList<>();
+ for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
+ //sort out all preferences that don't belong to the last provider
+ if (entry.getKey().startsWith(Provider.KEY + ".") ||
+ entry.getKey().startsWith(Provider.CA_CERT + ".") ||
+ entry.getKey().startsWith(Provider.CA_CERT_FINGERPRINT + ".")
+ ) {
+ continue;
+ }
+ lastProvidersKeys.add(entry.getKey());
+ }
+
+ SharedPreferences.Editor preferenceEditor = preferences.edit();
+ for (String key : lastProvidersKeys) {
+ preferenceEditor.remove(key);
+ }
+ preferenceEditor.apply();
+
switching_provider = false;
startActivityForResult(new Intent(this, ConfigurationWizard.class), Constants.REQUEST_CODE_SWITCH_PROVIDER);
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
index 8daa7d8c..52c797a4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java
@@ -31,6 +31,7 @@ public class DefaultedURL {
return url;
}
+ @Override
public String toString() {
return url.toString();
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
index d6d89b58..527ce1a7 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -19,9 +19,16 @@ package se.leap.bitmaskclient;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
+import android.support.v4.app.DialogFragment;
import android.content.DialogInterface;
import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
+
+import org.json.JSONObject;
+
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.DEFAULT;
+import static se.leap.bitmaskclient.DownloadFailedDialog.DOWNLOAD_ERRORS.valueOf;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORID;
+import static se.leap.bitmaskclient.ProviderAPI.ERRORS;
/**
* Implements a dialog to show why a download failed.
@@ -32,6 +39,13 @@ public class DownloadFailedDialog extends DialogFragment {
public static String TAG = "downloaded_failed_dialog";
private String reason_to_fail;
+ private DOWNLOAD_ERRORS downloadError = DEFAULT;
+ public enum DOWNLOAD_ERRORS {
+ DEFAULT,
+ ERROR_CORRUPTED_PROVIDER_JSON,
+ ERROR_INVALID_CERTIFICATE,
+ ERROR_CERTIFICATE_PINNING
+ }
/**
* @return a new instance of this DialogFragment.
@@ -42,32 +56,79 @@ public class DownloadFailedDialog extends DialogFragment {
return dialog_fragment;
}
+ /**
+ * @return a new instance of this DialogFragment.
+ */
+ public static DialogFragment newInstance(JSONObject errorJson) {
+ DownloadFailedDialog dialog_fragment = new DownloadFailedDialog();
+ try {
+ if (errorJson.has(ERRORS)) {
+ dialog_fragment.reason_to_fail = errorJson.getString(ERRORS);
+ } else {
+ //default error msg
+ dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message);
+ }
+
+ if (errorJson.has(ERRORID)) {
+ dialog_fragment.downloadError = valueOf(errorJson.getString(ERRORID));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ dialog_fragment.reason_to_fail = dialog_fragment.getString(R.string.error_io_exception_user_message);
+ }
+ return dialog_fragment;
+ }
+
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
-
builder.setMessage(reason_to_fail)
- .setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ interface_with_ConfigurationWizard.cancelSettingUpProvider();
+ dialog.dismiss();
+ }
+ });
+ switch (downloadError) {
+ case ERROR_CORRUPTED_PROVIDER_JSON:
+ builder.setPositiveButton(R.string.update_provider_details, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
dismiss();
- interface_with_ConfigurationWizard.retrySetUpProvider();
+ interface_with_ConfigurationWizard.updateProviderDetails();
}
- })
- .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int id) {
- interface_with_ConfigurationWizard.cancelSettingUpProvider();
- dialog.dismiss();
+ });
+ break;
+ case ERROR_CERTIFICATE_PINNING:
+ case ERROR_INVALID_CERTIFICATE:
+ builder.setPositiveButton(R.string.update_certificate, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dismiss();
+ interface_with_ConfigurationWizard.updateProviderDetails();
}
});
+ break;
+ default:
+ builder.setPositiveButton(R.string.retry, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dismiss();
+ interface_with_ConfigurationWizard.retrySetUpProvider();
+ }
+ });
+ break;
+ }
// Create the AlertDialog object and return it
return builder.create();
}
public interface DownloadFailedDialogInterface {
- public void retrySetUpProvider();
+ void retrySetUpProvider();
+
+ void cancelSettingUpProvider();
- public void cancelSettingUpProvider();
+ void updateProviderDetails();
}
DownloadFailedDialogInterface interface_with_ConfigurationWizard;
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/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 559b47d1..60b1b93c 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Provider.java
@@ -18,9 +18,11 @@ package se.leap.bitmaskclient;
import android.os.*;
+import com.google.gson.Gson;
+
import org.json.*;
-import java.io.*;
+import java.io.Serializable;
import java.net.*;
import java.util.*;
@@ -31,8 +33,11 @@ import java.util.*;
public final class Provider implements Parcelable {
private JSONObject definition = new JSONObject(); // Represents our Provider's provider.json
- private DefaultedURL main_url = new DefaultedURL();
- private String certificate_pin = "";
+ private DefaultedURL mainUrl = new DefaultedURL();
+ private DefaultedURL apiUrl = new DefaultedURL();
+ private String certificatePin = "";
+ private String certificatePinEncoding = "";
+ private String caCert = "";
final public static String
API_URL = "api_uri",
@@ -61,13 +66,24 @@ public final class Provider implements Parcelable {
public Provider() { }
- public Provider(URL main_url) {
- this.main_url.setUrl(main_url);
+ public Provider(URL mainUrl) {
+ this.mainUrl.setUrl(mainUrl);
}
- public Provider(URL main_url, String certificate_pin) {
- this.main_url.setUrl(main_url);
- this.certificate_pin = certificate_pin;
+ public Provider(URL mainUrl, String caCert, String definition) {
+ this.mainUrl.setUrl(mainUrl);
+ if (caCert != null) {
+ this.caCert = caCert;
+ }
+ if (definition != null) {
+ try {
+ this.definition = new JSONObject(definition);
+ parseDefinition(this.definition);
+ } catch (JSONException | NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
}
public static final Parcelable.Creator<Provider> CREATOR
@@ -81,42 +97,57 @@ public final class Provider implements Parcelable {
}
};
- private Provider(Parcel in) {
- try {
- main_url.setUrl(new URL(in.readString()));
- String definition_string = in.readString();
- if (!definition_string.isEmpty())
- definition = new JSONObject((definition_string));
- } catch (MalformedURLException | JSONException e) {
- e.printStackTrace();
- }
- }
-
public boolean isConfigured() {
- return !main_url.isDefault() && definition.length() > 0;
+ return !mainUrl.isDefault() &&
+ definition.length() > 0 &&
+ !apiUrl.isDefault() &&
+ caCert != null &&
+ !caCert.isEmpty();
}
protected void setUrl(URL url) {
- main_url.setUrl(url);
+ mainUrl.setUrl(url);
}
protected void define(JSONObject provider_json) {
definition = provider_json;
+ parseDefinition(definition);
}
- protected JSONObject definition() {
+ protected JSONObject getDefinition() {
return definition;
}
protected String getDomain() {
- return main_url.getDomain();
+ return mainUrl.getDomain();
+ }
+
+ protected DefaultedURL getMainUrl() {
+ return mainUrl;
+ }
+
+ protected DefaultedURL getApiUrl() {
+ return apiUrl;
+ }
+
+ protected String certificatePin() { return certificatePin; }
+
+ protected boolean hasCertificatePin() {
+ return certificatePin != null && !certificatePin.isEmpty();
+ }
+
+ boolean hasCaCert() {
+ return caCert != null && !caCert.isEmpty();
}
- protected DefaultedURL mainUrl() {
- return main_url;
+ public boolean hasDefinition() {
+ return definition != null && definition.length() > 0;
}
- protected String certificatePin() { return certificate_pin; }
+
+ public String getCaCert() {
+ return caCert;
+ }
public String getName() {
// Should we pass the locale in, or query the system here?
@@ -127,8 +158,8 @@ public final class Provider implements Parcelable {
name = definition.getJSONObject(API_TERM_NAME).getString(lang);
else throw new JSONException("Provider not defined");
} catch (JSONException e) {
- if (main_url != null) {
- String host = main_url.getDomain();
+ if (mainUrl != null) {
+ String host = mainUrl.getDomain();
name = host.substring(0, host.indexOf("."));
}
}
@@ -191,24 +222,29 @@ public final class Provider implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int i) {
- if(main_url != null)
- parcel.writeString(main_url.toString());
+ if(mainUrl != null)
+ parcel.writeString(mainUrl.toString());
if (definition != null)
parcel.writeString(definition.toString());
+ if (caCert != null)
+ parcel.writeString(caCert);
}
@Override
public boolean equals(Object o) {
if (o instanceof Provider) {
Provider p = (Provider) o;
- return p.mainUrl().getDomain().equals(mainUrl().getDomain());
+ return p.getDomain().equals(getDomain());
} else return false;
}
public JSONObject toJson() {
JSONObject json = new JSONObject();
try {
- json.put(Provider.MAIN_URL, main_url);
+ json.put(Provider.MAIN_URL, mainUrl);
+ //TODO: add other fields here?
+ //this is used to save custom providers as json. I guess this doesn't work correctly
+ //TODO 2: verify that
} catch (JSONException e) {
e.printStackTrace();
}
@@ -217,6 +253,45 @@ public final class Provider implements Parcelable {
@Override
public int hashCode() {
- return mainUrl().getDomain().hashCode();
+ return getDomain().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+
+ //TODO: write a test for marshalling!
+ private Provider(Parcel in) {
+ try {
+ mainUrl.setUrl(new URL(in.readString()));
+ String definitionString = in.readString();
+ if (!definitionString.isEmpty()) {
+ definition = new JSONObject((definitionString));
+ parseDefinition(definition);
+ }
+ String caCert = in.readString();
+ if (!caCert.isEmpty()) {
+ this.caCert = caCert;
+ }
+ } catch (MalformedURLException | JSONException e) {
+ e.printStackTrace();
+ }
}
+
+ private void parseDefinition(JSONObject definition) {
+ try {
+ String pin = definition.getString(CA_CERT_FINGERPRINT);
+ this.certificatePin = pin.split(":")[1].trim();
+ this.certificatePinEncoding = pin.split(":")[0].trim();
+ this.apiUrl.setUrl(new URL(definition.getString(API_URL)));
+ } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void setCACert(String cert) {
+ this.caCert = cert;
+ }
+
}
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..af79a95e
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiConnector.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2018 LEAP Encryption Access Project and contributers
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package se.leap.bitmaskclient;
+
+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();
+ //response code 401: already logged out
+ if (response.isSuccessful() || response.code() == 401) {
+ return true;
+ }
+ } catch (IOException | RuntimeException e) {
+ return false;
+ }
+
+ return false;
+ }
+
+ public static boolean canConnect(@NonNull OkHttpClient okHttpClient, String url) throws RuntimeException, IOException {
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method("GET", null);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ return response.isSuccessful();
+
+ }
+
+ public static String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) throws RuntimeException, IOException {
+
+ RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null;
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(url)
+ .method(request_method, jsonBody);
+ for (Pair<String, String> keyValPair : headerArgs) {
+ requestBuilder.addHeader(keyValPair.first, keyValPair.second);
+ }
+
+ //TODO: move to getHeaderArgs()?
+ String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
+ requestBuilder.addHeader("Accept-Language", locale);
+ Request request = requestBuilder.build();
+
+ Response response = okHttpClient.newCall(request).execute();
+ InputStream inputStream = response.body().byteStream();
+ Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
+ if (scanner.hasNext()) {
+ return scanner.next();
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiManagerBase.java
index 6e3b8b08..9f5fdc2d 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;
@@ -31,150 +30,140 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.io.InputStream;
import java.math.BigInteger;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.net.UnknownServiceException;
-import java.security.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;
+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 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.server_unreachable_message;
import static se.leap.bitmaskclient.R.string.service_is_down_error;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_cert;
+import static se.leap.bitmaskclient.R.string.warning_corrupted_provider_details;
+import static se.leap.bitmaskclient.R.string.warning_expired_provider_cert;
/**
- * 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",
- 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",
- 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;
-
- public 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;
- protected static String last_provider_main_url;
+ protected static String lastProviderMainUrl;
protected static boolean go_ahead = true;
protected static SharedPreferences preferences;
- protected static String provider_api_url;
- protected static String provider_ca_cert_fingerprint;
+ protected static String providerApiUrl;
+ protected static String providerCaCertFingerprint;
+ 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);
- }
-
- @Override
- public void onCreate() {
- super.onCreate();
-
- preferences = getSharedPreferences(Constants.SHARED_PREFERENCES, MODE_PRIVATE);
- resources = getResources();
+ public static String lastProviderMainUrl() {
+ return lastProviderMainUrl;
}
- public static String lastProviderMainUrl() {
- return last_provider_main_url;
+ 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);
- if (provider_api_url == null && preferences.contains(Provider.KEY)) {
+ if (providerApiUrl == null && preferences.contains(Provider.KEY)) {
try {
JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, ""));
- provider_api_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
+ providerApiUrl = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION);
go_ahead = true;
} catch (JSONException e) {
go_ahead = false;
}
}
- if (action.equalsIgnoreCase(SET_UP_PROVIDER)) {
+ if (action.equals(UPDATE_PROVIDER_DETAILS)) {
+ resetProviderDetails();
+ Bundle task = new Bundle();
+ task.putString(MAIN_URL, lastProviderMainUrl);
+ Bundle result = setUpProvider(task);
+ if (result.getBoolean(RESULT_KEY)) {
+ receiver.send(PROVIDER_OK, result);
+ } else {
+ receiver.send(PROVIDER_NOK, result);
+ }
+ } else if (action.equalsIgnoreCase(SET_UP_PROVIDER)) {
Bundle result = setUpProvider(parameters);
if (go_ahead) {
if (result.getBoolean(RESULT_KEY)) {
@@ -226,8 +215,15 @@ public abstract class ProviderApiBase extends IntentService {
}
}
+ protected void resetProviderDetails() {
+ CA_CERT_DOWNLOADED = PROVIDER_JSON_DOWNLOADED = false;
+ deleteProviderDetailsFromPreferences(providerDefinition);
+ providerCaCert = "";
+ providerDefinition = new JSONObject();
+ }
+
protected String formatErrorMessage(final int toastStringId) {
- return formatErrorMessage(getResources().getString(toastStringId));
+ return formatErrorMessage(resources.getString(toastStringId));
}
private String formatErrorMessage(String errorMessage) {
@@ -243,100 +239,24 @@ public abstract class ProviderApiBase extends IntentService {
}
}
- private JSONObject getErrorMessageAsJson(String message) {
+ protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage) {
try {
- return new JSONObject(formatErrorMessage(message));
+ jsonObject.put(ERRORS, errorMessage);
} catch (JSONException e) {
e.printStackTrace();
- return new JSONObject();
}
}
- private OkHttpClient initHttpClient(JSONObject initError, boolean isSelfSigned) {
- try {
- TLSCompatSocketFactory sslCompatFactory;
- ConnectionSpec spec = getConnectionSpec();
- OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
- if (isSelfSigned) {
- sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, ""));
- } else {
- sslCompatFactory = new TLSCompatSocketFactory();
- }
- sslCompatFactory.initSSLSocketFactory(clientBuilder);
- clientBuilder.cookieJar(getCookieJar())
- .connectionSpecs(Collections.singletonList(spec));
- return clientBuilder.build();
- } catch (IllegalStateException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
- } catch (KeyStoreException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
- } catch (KeyManagementException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
- } catch (NoSuchAlgorithmException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
- } catch (CertificateException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(certificate_error));
- } catch (UnknownHostException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(server_unreachable_message));
- } catch (IOException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_io_exception_user_message));
- } catch (NoSuchProviderException e) {
+ protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ jsonObject.put(ERRORID, errorId);
+ } catch (JSONException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
}
- return null;
- }
-
- protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, false);
- }
-
- protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, true);
}
- @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) {
@@ -366,7 +286,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);
}
@@ -376,7 +296,7 @@ public abstract class ProviderApiBase extends IntentService {
BigInteger password_verifier = client.calculateV(username, password, salt);
- JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
+ JSONObject api_result = sendNewUserDataToSRPServer(providerApiUrl, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient);
Bundle result = new Bundle();
if (api_result.has(ERRORS))
@@ -423,7 +343,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);
}
@@ -431,13 +351,13 @@ public abstract class ProviderApiBase extends IntentService {
LeapSRPSession client = new LeapSRPSession(username, password);
byte[] A = client.exponential();
- JSONObject step_result = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16), okHttpClient);
+ JSONObject step_result = sendAToSRPServer(providerApiUrl, username, new BigInteger(1, A).toString(16), okHttpClient);
try {
String salt = step_result.getString(LeapSRPSession.SALT);
byte[] Bbytes = new BigInteger(step_result.getString("B"), 16).toByteArray();
byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes);
if (M1 != null) {
- step_result = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient);
+ step_result = sendM1ToSRPServer(providerApiUrl, username, M1, okHttpClient);
setTokenIfAvailable(step_result);
byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray();
if (client.verify(M2)) {
@@ -461,7 +381,7 @@ public abstract class ProviderApiBase extends IntentService {
private boolean setTokenIfAvailable(JSONObject authentication_step_result) {
try {
LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN));
- } catch (JSONException e) { //
+ } catch (JSONException e) {
return false;
}
return true;
@@ -508,7 +428,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);
}
/**
@@ -589,13 +510,13 @@ public abstract class ProviderApiBase extends IntentService {
return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient);
}
- protected String sendGetStringToServer(String url, List<Pair<String, String>> headerArgs, OkHttpClient okHttpClient) {
+ protected String sendGetStringToServer(@NonNull String url, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient);
}
- private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
+ private JSONObject requestJsonFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
JSONObject responseJson;
String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient);
@@ -609,41 +530,19 @@ 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;
+ private String requestStringFromServer(@NonNull String url, @NonNull String request_method, String jsonString, @NonNull List<Pair<String, String>> headerArgs, @NonNull OkHttpClient okHttpClient) {
String plainResponseBody = null;
- 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();
-
try {
- response = okHttpClient.newCall(request).execute();
- 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 e) {
+ } catch (UnknownHostException | SocketTimeoutException e) {
plainResponseBody = formatErrorMessage(server_unreachable_message);
} catch (MalformedURLException e) {
plainResponseBody = formatErrorMessage(malformed_url);
- } catch (SocketTimeoutException e) {
- plainResponseBody = formatErrorMessage(server_unreachable_message);
} catch (SSLHandshakeException e) {
plainResponseBody = formatErrorMessage(certificate_error);
} catch (ConnectException e) {
@@ -660,6 +559,39 @@ public abstract class ProviderApiBase extends IntentService {
return plainResponseBody;
}
+ private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) {
+ JSONObject errorJson = new JSONObject();
+ String baseUrl = getApiUrl(providerDefinition);
+
+ OkHttpClient okHttpClient = clientGenerator.initSelfSignedCAHttpClient(errorJson, caCert);
+ if (okHttpClient == null) {
+ result.putString(ERRORS, errorJson.toString());
+ return false;
+ }
+
+ try {
+
+ return ProviderApiConnector.canConnect(okHttpClient, baseUrl);
+
+ } catch (UnknownHostException | SocketTimeoutException e) {
+ setErrorResult(result, server_unreachable_message, null);
+ } catch (MalformedURLException e) {
+ setErrorResult(result, malformed_url, null);
+ } catch (SSLHandshakeException e) {
+ setErrorResult(result, warning_corrupted_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ } catch (ConnectException e) {
+ setErrorResult(result, service_is_down_error, null);
+ } catch (IllegalArgumentException e) {
+ setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (UnknownServiceException e) {
+ //unable to find acceptable protocols - tlsv1.2 not enabled?
+ setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (IOException e) {
+ setErrorResult(result, error_io_exception_user_message, null);
+ }
+ return false;
+ }
+
/**
* Downloads a provider.json from a given URL, adding a new provider using the given name.
*
@@ -693,6 +625,7 @@ public abstract class ProviderApiBase extends IntentService {
} catch(JSONException e) {
return false;
} catch(NullPointerException e) {
+ e.printStackTrace();
return false;
}
}
@@ -707,18 +640,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 e) {
- result = false;
- } catch (NoSuchAlgorithmException e) {
- result = false;
- } catch (CertificateEncodingException e) {
+ } catch (JSONException | NoSuchAlgorithmException | CertificateEncodingException e) {
result = false;
}
}
@@ -726,16 +653,194 @@ public abstract class ProviderApiBase extends IntentService {
return result;
}
- private String base64toHex(String base64_input) {
- byte[] byteArray = Base64.decode(base64_input, Base64.DEFAULT);
- int readBytes = byteArray.length;
- StringBuffer hexData = new StringBuffer();
- int onebyte;
- for (int i = 0; i < readBytes; i++) {
- onebyte = ((0x000000ff & byteArray[i]) | 0xffffff00);
- hexData.append(Integer.toHexString(onebyte).substring(6));
+ protected void checkPersistedProviderUpdates() {
+ String providerDomain = getDomainFromMainURL(lastProviderMainUrl);
+ if (hasUpdatedProviderDetails(providerDomain)) {
+ providerCaCert = getPersistedProviderCA(providerDomain);
+ providerDefinition = getPersistedProviderDefinition(providerDomain);
+ providerCaCertFingerprint = getPersistedCaCertFingerprint(providerDomain);
+ providerApiUrl = getApiUrlWithVersion(providerDefinition);
}
- return hexData.toString();
+ }
+
+ protected Bundle validateProviderDetails() {
+ Bundle result = validateCertificateForProvider(providerCaCert, providerDefinition, lastProviderMainUrl);
+
+ //invalid certificate or no certificate
+ if (result.containsKey(ERRORS) || (result.containsKey(RESULT_KEY) && !result.getBoolean(RESULT_KEY)) ) {
+ return result;
+ }
+
+ //valid certificate: skip download, save loaded provider CA cert and provider definition directly
+ try {
+ preferences.edit().putString(Provider.KEY, providerDefinition.toString()).
+ putBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOW_ANONYMOUS)).
+ putBoolean(Constants.PROVIDER_ALLOWED_REGISTERED, providerDefinition.getJSONObject(Provider.SERVICE).getBoolean(Constants.PROVIDER_ALLOWED_REGISTERED)).
+ putString(Provider.CA_CERT, providerCaCert).commit();
+ CA_CERT_DOWNLOADED = true;
+ PROVIDER_JSON_DOWNLOADED = true;
+ result.putBoolean(RESULT_KEY, true);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ }
+
+ return result;
+ }
+
+ protected Bundle validateCertificateForProvider(String cert_string, JSONObject providerDefinition, String mainUrl) {
+ Bundle result = new Bundle();
+ result.putBoolean(RESULT_KEY, false);
+
+ if (ConfigHelper.checkErroneousDownload(cert_string)) {
+ return result;
+ }
+
+ X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string);
+ if (certificate == null) {
+ return setErrorResult(result, 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 = getFingerprintFromCertificate(certificate, encoding);
+ if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) {
+ return setErrorResult(result, warning_corrupted_provider_cert, ERROR_CERTIFICATE_PINNING.toString());
+ }
+
+
+ if (!hasApiUrlExpectedDomain(providerDefinition, mainUrl)){
+ return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ }
+
+ if (!canConnect(cert_string, providerDefinition, result)) {
+ return result;
+ }
+ } catch (NoSuchAlgorithmException e ) {
+ return setErrorResult(result, error_no_such_algorithm_exception_user_message, null);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ return setErrorResult(result, warning_corrupted_provider_details, ERROR_CORRUPTED_PROVIDER_JSON.toString());
+ } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
+ return setErrorResult(result, warning_expired_provider_cert, ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ result.putBoolean(RESULT_KEY, true);
+ return result;
+ }
+
+ protected Bundle setErrorResult(Bundle result, int errorMessageId, String errorId) {
+ JSONObject errorJson = new JSONObject();
+ if (errorId != null) {
+ addErrorMessageToJson(errorJson, resources.getString(errorMessageId), errorId);
+ } else {
+ addErrorMessageToJson(errorJson, resources.getString(errorMessageId));
+ }
+ result.putString(ERRORS, errorJson.toString());
+ result.putBoolean(RESULT_KEY, false);
+ return result;
+ }
+
+ /**
+ * This method aims to prevent attacks where the provider.json file got manipulated by a third party.
+ * The main url is visible to the provider when setting up a new provider.
+ * The user is responsible to check that this is the provider main url he intends to connect to.
+ *
+ * @param providerDefinition
+ * @param mainUrlString
+ * @return
+ */
+ private boolean hasApiUrlExpectedDomain(JSONObject providerDefinition, String mainUrlString) {
+ // fix against "api_uri": "https://calyx.net.malicious.url.net:4430",
+ String apiUrlString = getApiUrl(providerDefinition);
+ String providerDomain = getProviderDomain(providerDefinition);
+ if (mainUrlString.contains(providerDomain) && apiUrlString.contains(providerDomain + ":")) {
+ return true;
+ }
+ return false;
+ }
+
+ protected String getCaCertFingerprint(JSONObject providerDefinition) {
+ try {
+ return providerDefinition.getString(Provider.CA_CERT_FINGERPRINT);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ protected String getApiUrl(JSONObject providerDefinition) {
+ try {
+ return providerDefinition.getString(Provider.API_URL);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ protected String getApiUrlWithVersion(JSONObject providerDefinition) {
+ try {
+ return providerDefinition.getString(Provider.API_URL) + "/" + providerDefinition.getString(Provider.API_VERSION);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ protected void deleteProviderDetailsFromPreferences(JSONObject providerDefinition) {
+ String providerDomain = getProviderDomain(providerDefinition);
+
+ if (preferences.contains(Provider.KEY + "." + providerDomain)) {
+ preferences.edit().remove(Provider.KEY + "." + providerDomain).apply();
+ }
+ if (preferences.contains(Provider.CA_CERT + "." + providerDomain)) {
+ preferences.edit().remove(Provider.CA_CERT + "." + providerDomain).apply();
+ }
+ if (preferences.contains(Provider.CA_CERT_FINGERPRINT + "." + providerDomain)) {
+ preferences.edit().remove(Provider.CA_CERT_FINGERPRINT + "." + providerDomain).apply();
+ }
+ }
+
+ protected String getPersistedCaCertFingerprint(String providerDomain) {
+ try {
+ return getPersistedProviderDefinition(providerDomain).getString(Provider.CA_CERT_FINGERPRINT);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ return "";
+ }
+
+ protected JSONObject getPersistedProviderDefinition(String providerDomain) {
+ try {
+ return new JSONObject(preferences.getString(Provider.KEY + "." + providerDomain, ""));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return new JSONObject();
+ }
+ }
+
+ protected String getPersistedProviderCA(String providerDomain) {
+ return preferences.getString(Provider.CA_CERT + "." + providerDomain, "");
+ }
+
+ protected String getProviderDomain(JSONObject providerDefinition) {
+ try {
+ return providerDefinition.getString(Provider.DOMAIN);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
+ return "";
+ }
+
+ 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("/.*", "");
+
}
/**
@@ -753,6 +858,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;
@@ -769,31 +876,20 @@ 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;
}
- String deleteUrl = provider_api_url + "/logout";
+ String deleteUrl = providerApiUrl + "/logout";
int progress = 0;
- 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;
+ 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/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
index abbdeb66..92d5da9f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
@@ -1,20 +1,31 @@
package se.leap.bitmaskclient;
-import android.content.res.*;
-
-import com.pedrogomez.renderers.*;
-
-import org.json.*;
-
-import java.io.*;
-import java.net.*;
-import java.util.*;
+import android.content.res.AssetManager;
+
+import com.pedrogomez.renderers.AdapteeCollection;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
/**
* Created by parmegv on 4/12/14.
*/
public class ProviderManager implements AdapteeCollection<Provider> {
+ private static final String TAG = ProviderManager.class.getName();
private AssetManager assets_manager;
private File external_files_dir;
private Set<Provider> default_providers;
@@ -47,19 +58,27 @@ public class ProviderManager implements AdapteeCollection<Provider> {
private Set<Provider> providersFromAssets(String directory, String[] relative_file_paths) {
Set<Provider> providers = new HashSet<Provider>();
- try {
+
for (String file : relative_file_paths) {
- InputStream provider_file = assets_manager.open(directory + "/" + file);
- String main_url = extractMainUrlFromInputStream(provider_file);
- String certificate_pin = extractCertificatePinFromInputStream(provider_file);
- if(certificate_pin.isEmpty())
- providers.add(new Provider(new URL(main_url)));
- else
- providers.add(new Provider(new URL(main_url), certificate_pin));
+ String mainUrl = null;
+ String certificate = null;
+ String providerDefinition = null;
+ try {
+ String provider = file.substring(0, file.length() - ".url".length());
+ InputStream provider_file = assets_manager.open(directory + "/" + file);
+ mainUrl = extractMainUrlFromInputStream(provider_file);
+ certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem"));
+ providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json"));
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ try {
+ providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition));
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ }
}
- } catch (IOException e) {
- e.printStackTrace();
- }
+
return providers;
}
@@ -89,21 +108,11 @@ public class ProviderManager implements AdapteeCollection<Provider> {
String main_url = "";
JSONObject file_contents = inputStreamToJson(input_stream);
- if(file_contents != null)
+ if (file_contents != null)
main_url = file_contents.optString(Provider.MAIN_URL);
return main_url;
}
- private String extractCertificatePinFromInputStream(InputStream input_stream) {
- String certificate_pin = "";
-
- JSONObject file_contents = inputStreamToJson(input_stream);
- if(file_contents != null)
- certificate_pin = file_contents.optString(Provider.CA_CERT_FINGERPRINT);
-
- return certificate_pin;
- }
-
private JSONObject inputStreamToJson(InputStream input_stream) {
JSONObject json = null;
try {
diff --git a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
index 55760cb3..856b104b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/StartActivity.java
@@ -62,7 +62,7 @@ public class StartActivity extends Activity {
}
// initialize app necessities
- ProviderAPICommand.initialize(this);
+ ProviderAPICommand.initialize(getApplicationContext());
VpnStatus.initLogCache(getApplicationContext().getCacheDir());
User.init(getString(R.string.default_username));
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 134d67ab..5b9aabef 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -96,4 +96,9 @@
<string name="void_vpn_error_establish">Failed to establish blocking VPN.</string>
<string name="void_vpn_stopped">Stopped blocking all outgoing internet traffic.</string>
<string name="void_vpn_title">Blocking traffic</string>
+ <string name="update_provider_details">Update provider details</string>
+ <string name="update_certificate">Update certificate</string>
+ <string name="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.</string>
+ <string name="warning_corrupted_provider_cert">Stored provider certificate is invalid. You can either update Bitmask (recommended) or update the provider certificate using a commercial CA certificate.</string>
+ <string name="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.</string>
</resources>