summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java26
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java49
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Dashboard.java37
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DefaultedURL.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java86
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/Provider.java136
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java303
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/ProviderManager.java53
-rw-r--r--app/src/main/res/values/strings.xml5
9 files changed, 558 insertions, 138 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
index 21520dc4..1d675499 100644
--- a/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/BaseConfigurationWizard.java
@@ -56,6 +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;
/**
* abstract base Activity that builds and shows the list of known available providers.
@@ -184,10 +185,10 @@ public abstract class BaseConfigurationWizard extends Activity
// 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);
}
@@ -231,12 +232,11 @@ public abstract class BaseConfigurationWizard extends Activity
}
} 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);
@@ -293,7 +293,9 @@ public abstract class BaseConfigurationWizard extends Activity
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();
@@ -369,18 +371,24 @@ public abstract class BaseConfigurationWizard extends Activity
/**
* 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);
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
index fd1e2080..ed527a54 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ConfigHelper.java
@@ -27,6 +27,8 @@ import java.security.cert.*;
import java.security.interfaces.*;
import java.security.spec.*;
+import static android.R.attr.name;
+
/**
* Stores constants, and implements auxiliary methods used across all LEAP Android classes.
*
@@ -34,6 +36,7 @@ import java.security.spec.*;
* @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 +45,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;
@@ -99,6 +102,38 @@ public class ConfigHelper {
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 {
@@ -126,6 +161,18 @@ public class ConfigHelper {
return key;
}
+ public static 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));
+ }
+ return hexData.toString();
+ }
+
/**
* 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 f1e7b3bd..861ce801 100644
--- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
+++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java
@@ -38,9 +38,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 de.blinkt.openvpn.core.VpnStatus;
import se.leap.bitmaskclient.userstatus.SessionDialog;
import se.leap.bitmaskclient.userstatus.User;
import se.leap.bitmaskclient.userstatus.UserStatusFragment;
@@ -104,6 +108,11 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
handleVersion();
}
+ // initialize app necessities
+ ProviderAPICommand.initialize(this);
+ VpnStatus.initLogCache(getApplicationContext().getCacheDir());
+ User.init(getString(R.string.default_username));
+
prepareEIP(savedInstanceState);
}
@@ -147,6 +156,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
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();
}
@@ -246,10 +256,9 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
}
@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() {
@@ -399,7 +408,25 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec
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), 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 da32dbd4..6f6a14de 100644
--- a/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/DownloadFailedDialog.java
@@ -20,6 +20,16 @@ import android.app.*;
import android.content.*;
import android.os.*;
+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;
+
/**
* Implements a dialog to show why a download failed.
*
@@ -29,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.
@@ -39,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/Provider.java b/app/src/main/java/se/leap/bitmaskclient/Provider.java
index 559b47d1..71a0e149 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,20 @@ 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 certificatePin,*/ String definition) {
+ this.mainUrl.setUrl(mainUrl);
+ this.caCert = caCert;
+ try {
+ this.definition = new JSONObject(definition);
+ parseDefinition(this.definition);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+
}
public static final Parcelable.Creator<Provider> CREATOR
@@ -81,42 +93,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 +154,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 +218,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.getMainUrl().getDomain().equals(getMainUrl().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 +249,44 @@ public final class Provider implements Parcelable {
@Override
public int hashCode() {
- return mainUrl().getDomain().hashCode();
+ return getMainUrl().getDomain().hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return new Gson().toJson(this);
+ }
+
+ 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/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
index 6e3b8b08..dfc48bee 100644
--- a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
+++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java
@@ -45,6 +45,8 @@ 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;
@@ -71,6 +73,12 @@ 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.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.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;
@@ -96,6 +104,7 @@ 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",
@@ -105,6 +114,7 @@ public abstract class ProviderApiBase extends IntentService {
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";
@@ -123,16 +133,18 @@ public abstract class ProviderApiBase extends IntentService {
CORRECTLY_DOWNLOADED_EIP_SERVICE = 13,
INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14;
- public static boolean
+ protected static 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;
public static void stop() {
@@ -155,7 +167,7 @@ public abstract class ProviderApiBase extends IntentService {
}
public static String lastProviderMainUrl() {
- return last_provider_main_url;
+ return lastProviderMainUrl;
}
@Override
@@ -164,17 +176,26 @@ public abstract class ProviderApiBase extends IntentService {
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,6 +247,13 @@ 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));
}
@@ -243,22 +271,31 @@ 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) {
+
+ protected void addErrorMessageToJson(JSONObject jsonObject, String errorMessage, String errorId) {
+ try {
+ jsonObject.put(ERRORS, errorMessage);
+ jsonObject.put(ERRORID, errorId);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private OkHttpClient initHttpClient(JSONObject initError, String certificate) {
try {
TLSCompatSocketFactory sslCompatFactory;
ConnectionSpec spec = getConnectionSpec();
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
- if (isSelfSigned) {
- sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, ""));
+ if (!isEmpty(certificate)) {
+ sslCompatFactory = new TLSCompatSocketFactory(certificate);
} else {
sslCompatFactory = new TLSCompatSocketFactory();
}
@@ -266,40 +303,39 @@ public abstract class ProviderApiBase extends IntentService {
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) {
+ } catch (IllegalArgumentException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
- } catch (KeyManagementException e) {
+ addErrorMessageToJson(initError, resources.getString(R.string.certificate_error));
+ } catch (IllegalStateException | KeyManagementException | KeyStoreException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
- } catch (NoSuchAlgorithmException e) {
+ addErrorMessageToJson(initError, String.format(resources.getString(keyChainAccessError), e.getLocalizedMessage()));
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
+ addErrorMessageToJson(initError, resources.getString(error_no_such_algorithm_exception_user_message));
} catch (CertificateException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(certificate_error));
+ addErrorMessageToJson(initError, resources.getString(certificate_error));
} catch (UnknownHostException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(server_unreachable_message));
+ addErrorMessageToJson(initError, resources.getString(server_unreachable_message));
} catch (IOException e) {
e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_io_exception_user_message));
- } catch (NoSuchProviderException e) {
- e.printStackTrace();
- initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message));
+ addErrorMessageToJson(initError, resources.getString(error_io_exception_user_message));
}
return null;
}
protected OkHttpClient initCommercialCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, false);
+ return initHttpClient(initError, null);
}
protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) {
- return initHttpClient(initError, true);
+ String certificate = preferences.getString(Provider.CA_CERT, "");
+ return initHttpClient(initError, certificate);
+ }
+
+ protected OkHttpClient initSelfSignedCAHttpClient(JSONObject initError, String certificate) {
+ return initHttpClient(initError, certificate);
}
@NonNull
@@ -376,7 +412,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))
@@ -431,13 +467,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)) {
@@ -629,6 +665,9 @@ public abstract class ProviderApiBase extends IntentService {
try {
response = okHttpClient.newCall(request).execute();
+ if (!response.isSuccessful()){
+ return formatErrorMessage(error_json_exception_user_message);
+ }
InputStream inputStream = response.body().byteStream();
Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
@@ -638,12 +677,10 @@ public abstract class ProviderApiBase extends IntentService {
} 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) {
@@ -693,6 +730,7 @@ public abstract class ProviderApiBase extends IntentService {
} catch(JSONException e) {
return false;
} catch(NullPointerException e) {
+ e.printStackTrace();
return false;
}
}
@@ -714,11 +752,7 @@ public abstract class ProviderApiBase extends IntentService {
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 +760,179 @@ 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 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, 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));
+ if (!real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim())) {
+ return setErrorResult(result, 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());
+ }
+
+ if (!canConnect(cert_string, providerDefinition, result)) {
+ return result;
+ }
+ } 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());
+ } catch (CertificateEncodingException | CertificateNotYetValidException | CertificateExpiredException e) {
+ return setErrorResult(result, getString(R.string.warning_expired_provider_cert), ERROR_INVALID_CERTIFICATE.toString());
+ }
+
+ result.putBoolean(RESULT_KEY, true);
+ return result;
+ }
+
+ protected Bundle setErrorResult(Bundle result, String errorMessage, String errorId) {
+ JSONObject errorJson = new JSONObject();
+ if (errorId != null) {
+ addErrorMessageToJson(errorJson, errorMessage, errorId);
+ } else {
+ addErrorMessageToJson(errorJson, errorMessage);
+ }
+ result.putString(ERRORS, errorJson.toString());
+ 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;
+ }
+
+ private boolean canConnect(String caCert, JSONObject providerDefinition, Bundle result) {
+ JSONObject errorJson = new JSONObject();
+ String baseUrl = getApiUrl(providerDefinition);
+
+ OkHttpClient okHttpClient = initSelfSignedCAHttpClient(errorJson, caCert);
+ if (okHttpClient == null) {
+ result.putString(ERRORS, errorJson.toString());
+ return false;
}
- return hexData.toString();
+
+ List<Pair<String, String>> headerArgs = getAuthorizationHeader();
+ String plain_response = requestStringFromServer(baseUrl, "GET", null, headerArgs, okHttpClient);
+
+ try {
+ if (new JSONObject(plain_response).has(ERRORS)) {
+ result.putString(ERRORS, plain_response);
+ return false;
+ }
+ } catch (JSONException e) {
+ //eat me
+ }
+
+ return true;
+ }
+
+ 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 providerDomain) {
+ return preferences.contains(Provider.KEY + "." + providerDomain) && preferences.contains(Provider.CA_CERT + "." + providerDomain);
}
/**
@@ -774,7 +971,7 @@ public abstract class ProviderApiBase extends IntentService {
return false;
}
- String deleteUrl = provider_api_url + "/logout";
+ String deleteUrl = providerApiUrl + "/logout";
int progress = 0;
Request.Builder requestBuilder = new Request.Builder()
diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java b/app/src/main/java/se/leap/bitmaskclient/ProviderManager.java
index abbdeb66..cf703631 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;
@@ -49,13 +60,13 @@ public class ProviderManager implements AdapteeCollection<Provider> {
Set<Provider> providers = new HashSet<Provider>();
try {
for (String file : relative_file_paths) {
+
+ String provider = file.substring(0, file.length() - ".url".length());
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 = extractMainUrlFromInputStream(provider_file);
+ String certificate = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".pem"));
+ String providerDefinition = ConfigHelper.loadInputStreamAsString(assets_manager.open(provider + ".json"));
+ providers.add(new Provider(new URL(mainUrl), certificate, providerDefinition));
}
} catch (IOException e) {
e.printStackTrace();
@@ -89,21 +100,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/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 70f6f4ab..680f92e1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -82,4 +82,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 corrupted. 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>