diff options
Diffstat (limited to 'app/src/main')
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> |