From c37149dec7dbc2ff2bccfa643792080c3c86ce18 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Wed, 25 Oct 2017 15:55:49 +0200 Subject: 8757 fixes session cookie handling by implementing okHttpClient and custom cookiejar, enables TLS 1.2 on old devices, restricts allowed cipher suites on new devices in order to harden tls based communication --- app/build.gradle | 1 + .../java/se/leap/bitmaskclient/ProviderAPI.java | 407 ++++++++++++++------- app/src/main/AndroidManifest.xml | 3 +- .../java/se/leap/bitmaskclient/BitmaskApp.java | 17 + .../main/java/se/leap/bitmaskclient/Dashboard.java | 51 ++- .../java/se/leap/bitmaskclient/SrpCredentials.java | 26 ++ .../se/leap/bitmaskclient/SrpRegistrationData.java | 42 +++ .../leap/bitmaskclient/TLSCompatSocketFactory.java | 133 +++++++ .../bitmaskclient/userstatus/SessionDialog.java | 15 +- app/src/main/res/values-es/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 11 files changed, 534 insertions(+), 165 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/BitmaskApp.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/SrpCredentials.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/SrpRegistrationData.java create mode 100644 app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java diff --git a/app/build.gradle b/app/build.gradle index 2614c6fb..7f838cdf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,6 +60,7 @@ dependencies { compile 'com.intellij:annotations:12.0' compile 'com.google.code.gson:gson:2.4' compile 'org.thoughtcrime.ssl.pinning:AndroidPinning:1.0.0' + compile 'com.squareup.okhttp3:okhttp:3.9.0' compile 'mbanje.kurt:fabbutton:1.1.4' compile 'com.android.support:support-annotations:25.3.1' compile 'com.android.support:support-v4:26.0.0-alpha1' diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index a1b1b383..4805456c 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -16,31 +16,89 @@ */ package se.leap.bitmaskclient; -import android.app.*; -import android.content.*; -import android.content.res.*; -import android.os.*; +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; import android.util.Base64; -import org.json.*; -import org.thoughtcrime.ssl.pinning.util.*; - -import java.io.*; -import java.math.*; -import java.net.*; -import java.security.*; -import java.security.cert.*; -import java.security.interfaces.*; -import java.util.*; - -import javax.net.ssl.*; - -import se.leap.bitmaskclient.ProviderListContent.*; -import se.leap.bitmaskclient.eip.*; +import org.json.JSONException; +import org.json.JSONObject; +import org.thoughtcrime.ssl.pinning.util.PinningHelper; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.net.ConnectException; +import java.net.CookieHandler; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.net.UnknownServiceException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Scanner; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +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.ProviderListContent.ProviderItem; +import se.leap.bitmaskclient.eip.Constants; +import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatus; +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; + /** * Implements HTTP api methods used to manage communications with the provider server. *

@@ -79,7 +137,8 @@ public class ProviderAPI extends IntentService { PROVIDER_OK = 11, PROVIDER_NOK = 12, CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, - INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14; + INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14, + INITIALIZATION_ERROR = 15; private static boolean CA_CERT_DOWNLOADED = false, @@ -93,11 +152,15 @@ public class ProviderAPI extends IntentService { private static String provider_api_url; private static String provider_ca_cert_fingerprint; private Resources resources; - public static void stop() { go_ahead = false; } + private final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + private OkHttpClient okHttpClient; + private String initializationError = null; + public ProviderAPI() { super(TAG); } @@ -106,10 +169,9 @@ public class ProviderAPI extends IntentService { public void onCreate() { super.onCreate(); - preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); resources = getResources(); - CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)); + initOkHttpClient(); } public static String lastProviderMainUrl() { @@ -120,13 +182,19 @@ public class ProviderAPI extends IntentService { return last_danger_on; } - private String formatErrorMessage(final int toast_string_id) { - return "{ \"" + ERRORS + "\" : \"" + getResources().getString(toast_string_id) + "\" }"; - } @Override protected void onHandleIntent(Intent command) { final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); + + if (initializationError != null) { + Bundle result = new Bundle(); + result.putString(SessionDialog.ERRORS.INITIALIZATION_ERROR.toString(), initializationError); + result.putBoolean(RESULT_KEY, false); + receiver.send(INITIALIZATION_ERROR, result); + return; + } + String action = command.getAction(); Bundle parameters = command.getBundleExtra(PARAMETERS); if (provider_api_url == null && preferences.contains(Provider.KEY)) { @@ -190,6 +258,96 @@ public class ProviderAPI extends IntentService { } } + private String formatErrorMessage(final int toastStringId) { + return "{ \"" + ERRORS + "\" : \"" + getResources().getString(toastStringId) + "\" }"; + } + + private JSONObject getErrorMessageAsJson(final int toastStringId) { + try { + return new JSONObject(formatErrorMessage(toastStringId)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + private void initOkHttpClient() { + try { + + ConnectionSpec spec = getConnectionSpec(); + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); + initSSLSocketFactory(clientBuilder); + clientBuilder.cookieJar(getCookieJar()) + .connectionSpecs(Collections.singletonList(spec)); + okHttpClient = clientBuilder.build(); + + } catch (IllegalStateException e) { + e.printStackTrace(); + //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); + initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + } catch (KeyStoreException e) { + e.printStackTrace(); + //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); + initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + } catch (KeyManagementException e) { + e.printStackTrace(); + //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); + initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + //initializationError = formatErrorMessage(error_no_such_algorithm_exception_user_message); + initializationError = getResources().getString(error_no_such_algorithm_exception_user_message); + } catch (CertificateException e) { + e.printStackTrace(); + initializationError = getResources().getString(certificate_error); + } catch (UnknownHostException e) { + e.printStackTrace(); + initializationError = getResources().getString(server_unreachable_message); + } catch (IOException e) { + e.printStackTrace(); + initializationError = getResources().getString(error_io_exception_user_message); + } catch (NoSuchProviderException e) { + e.printStackTrace(); + } + } + + @NonNull + private ConnectionSpec getConnectionSpec() { + ConnectionSpec.Builder connectionSpecbuilder = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3); + //FIXME: restrict connection further to the following recommended cipher suites for ALL supported API levels + //figure out how to use bcjsse for that purpose + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) + connectionSpecbuilder.cipherSuites( + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + ); + return connectionSpecbuilder.build(); + } + + @NonNull + private CookieJar getCookieJar() { + return new CookieJar() { + private final HashMap> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + private Bundle tryToRegister(Bundle task) { Bundle result = new Bundle(); int progress = 0; @@ -310,20 +468,33 @@ public class ProviderAPI extends IntentService { } private Bundle authFailedNotification(JSONObject result, String username) { - Bundle user_notification_bundle = new Bundle(); - try { - JSONObject error_message = result.getJSONObject(ERRORS); - String error_type = error_message.keys().next().toString(); - String message = error_message.get(error_type).toString(); - user_notification_bundle.putString(getResources().getString(R.string.user_message), message); - } catch (JSONException e) { + Bundle userNotificationBundle = new Bundle(); + Object baseErrorMessage = result.opt(ERRORS); + if (baseErrorMessage != null) { + if (baseErrorMessage instanceof JSONObject) { + try { + JSONObject errorMessage = result.getJSONObject(ERRORS); + String errorType = errorMessage.keys().next().toString(); + String message = errorMessage.get(errorType).toString(); + userNotificationBundle.putString(getResources().getString(R.string.user_message), message); + } catch (JSONException e) { + e.printStackTrace(); + } + } else if (baseErrorMessage instanceof String) { + try { + String errorMessage = result.getString(ERRORS); + userNotificationBundle.putString(getResources().getString(R.string.user_message), errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } } if (!username.isEmpty()) - user_notification_bundle.putString(SessionDialog.USERNAME, username); - user_notification_bundle.putBoolean(RESULT_KEY, false); + userNotificationBundle.putString(SessionDialog.USERNAME, username); + userNotificationBundle.putBoolean(RESULT_KEY, false); - return user_notification_bundle; + return userNotificationBundle; } /** @@ -374,10 +545,8 @@ public class ProviderAPI extends IntentService { * @return response from authentication server */ private JSONObject sendAToSRPServer(String server_url, String username, String clientA) { - Map parameters = new HashMap(); - parameters.put("login", username); - parameters.put("A", clientA); - return sendToServer(server_url + "/sessions.json", "POST", parameters); + SrpCredentials srpCredentials = new SrpCredentials(username, clientA); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString()); } /** @@ -389,10 +558,9 @@ public class ProviderAPI extends IntentService { * @return response from authentication server */ private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1) { - Map parameters = new HashMap(); - parameters.put("client_auth", new BigInteger(1, ConfigHelper.trim(m1)).toString(16)); - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", parameters); + String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json); } /** @@ -405,100 +573,52 @@ public class ProviderAPI extends IntentService { * @return response from authentication server */ private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier) { - Map parameters = new HashMap(); - parameters.put("user[login]", username); - parameters.put("user[password_salt]", salt); - parameters.put("user[password_verifier]", password_verifier); - return sendToServer(server_url + "/users.json", "POST", parameters); + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString()); } - /** - * Executes an HTTP request expecting a JSON response. - * - * @param url - * @param request_method - * @param parameters - * @return response from authentication server - */ - private JSONObject sendToServer(String url, String request_method, Map parameters) { - JSONObject json_response; - HttpsURLConnection urlConnection = null; - try { - InputStream is = null; - urlConnection = (HttpsURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod(request_method); - String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); - urlConnection.setRequestProperty("Accept-Language", locale); - urlConnection.setChunkedStreamingMode(0); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + private JSONObject sendToServer(String url, String request_method, String jsonString) { + Response response; + JSONObject responseJson = new JSONObject(); - DataOutputStream writer = new DataOutputStream(urlConnection.getOutputStream()); - writer.writeBytes(formatHttpParameters(parameters)); - writer.close(); + RequestBody jsonBody = RequestBody.create(JSON, jsonString); - is = urlConnection.getInputStream(); - String plain_response = new Scanner(is).useDelimiter("\\A").next(); - json_response = new JSONObject(plain_response); - } catch (IOException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (JSONException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (KeyManagementException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (KeyStoreException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (CertificateException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } + Request request = new Request.Builder() + .url(url) + .method(request_method, jsonBody) + .build(); - return json_response; - } + try { + response = okHttpClient.newCall(request).execute(); - private JSONObject getErrorMessage(HttpsURLConnection urlConnection) { - JSONObject error_message = new JSONObject(); - if (urlConnection != null) { - InputStream error_stream = urlConnection.getErrorStream(); - if (error_stream != null) { - String error_response = new Scanner(error_stream).useDelimiter("\\A").next(); - try { - error_message = new JSONObject(error_response); - } catch (JSONException e) { - e.printStackTrace(); - } - urlConnection.disconnect(); + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + String plain_response = scanner.next(); + responseJson = new JSONObject(plain_response); } - } - return error_message; - } - - private String formatHttpParameters(Map parameters) throws UnsupportedEncodingException { - StringBuilder result = new StringBuilder(); - boolean first = true; - Iterator parameter_iterator = parameters.keySet().iterator(); - while (parameter_iterator.hasNext()) { - if (first) - first = false; - else - result.append("&&"); - - String key = parameter_iterator.next(); - String value = parameters.get(key); - - result.append(URLEncoder.encode(key, "UTF-8")); - result.append("="); - result.append(URLEncoder.encode(value, "UTF-8")); + } catch (JSONException e) { + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } catch (NullPointerException npe) { + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } catch (UnknownHostException e) { + responseJson = getErrorMessageAsJson(server_unreachable_message); + } catch (MalformedURLException e) { + responseJson = getErrorMessageAsJson(malformed_url); + } catch (SocketTimeoutException e) { + responseJson = getErrorMessageAsJson(server_unreachable_message); + } catch (SSLHandshakeException e) { + responseJson = getErrorMessageAsJson(certificate_error); + } catch (ConnectException e) { + responseJson = getErrorMessageAsJson(service_is_down_error); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + responseJson = getErrorMessageAsJson(error_no_such_algorithm_exception_user_message); + } catch (IOException e) { + responseJson = getErrorMessageAsJson(error_io_exception_user_message); } - return result.toString(); + return responseJson; } @@ -561,7 +681,7 @@ public class ProviderAPI extends IntentService { result.putBoolean(RESULT_KEY, false); } } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(R.string.malformed_url); + String reason_to_fail = formatErrorMessage(malformed_url); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); } @@ -705,7 +825,7 @@ public class ProviderAPI extends IntentService { result = danger_on ? downloadWithoutCA(url_string) : formatErrorMessage(R.string.error_security_pinnedcertificate); } else - result = formatErrorMessage(R.string.error_io_exception_user_message); + result = formatErrorMessage(error_io_exception_user_message); } return result; @@ -735,19 +855,19 @@ public class ProviderAPI extends IntentService { url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next(); } catch (MalformedURLException e) { - json_file_content = formatErrorMessage(R.string.malformed_url); + json_file_content = formatErrorMessage(malformed_url); } catch (SocketTimeoutException e) { - json_file_content = formatErrorMessage(R.string.server_unreachable_message); + json_file_content = formatErrorMessage(server_unreachable_message); } catch (SSLHandshakeException e) { if (provider_url != null) { json_file_content = downloadWithProviderCA(string_url, danger_on); } else { - json_file_content = formatErrorMessage(R.string.certificate_error); + json_file_content = formatErrorMessage(certificate_error); } } catch (ConnectException e) { - json_file_content = formatErrorMessage(R.string.service_is_down_error); + json_file_content = formatErrorMessage(service_is_down_error); } catch (FileNotFoundException e) { - json_file_content = formatErrorMessage(R.string.malformed_url); + json_file_content = formatErrorMessage(malformed_url); } catch (Exception e) { if (provider_url != null && danger_on) { json_file_content = downloadWithProviderCA(string_url, danger_on); @@ -764,6 +884,7 @@ public class ProviderAPI extends IntentService { * @param danger_on true to download CA certificate in case it has not been downloaded. * @return an empty string if it fails, the url content if not. */ + //FIXME: refactor and use okHttpClient instead! private String downloadWithProviderCA(String url_string, boolean danger_on) { String json_file_content = ""; @@ -781,13 +902,13 @@ public class ProviderAPI extends IntentService { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); - json_file_content = formatErrorMessage(R.string.server_unreachable_message); + json_file_content = formatErrorMessage(server_unreachable_message); } catch (IOException e) { // The downloaded certificate doesn't validate our https connection. if (danger_on) { json_file_content = downloadWithoutCA(url_string); } else { - json_file_content = formatErrorMessage(R.string.certificate_error); + json_file_content = formatErrorMessage(certificate_error); } } catch (KeyStoreException e) { // TODO Auto-generated catch block @@ -800,11 +921,12 @@ public class ProviderAPI extends IntentService { e.printStackTrace(); } catch (NoSuchElementException e) { e.printStackTrace(); - json_file_content = formatErrorMessage(R.string.server_unreachable_message); + json_file_content = formatErrorMessage(server_unreachable_message); } return json_file_content; } + @Deprecated private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException { String provider_cert_string = preferences.getString(Provider.CA_CERT, ""); @@ -828,9 +950,15 @@ public class ProviderAPI extends IntentService { return context.getSocketFactory(); } + private void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException, NoSuchProviderException { + TLSCompatSocketFactory sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, "")); + sslCompatFactory.initSSLSocketFactory(builder); + } + /** * Downloads the string that's in the url with any certificate. */ + // FIXME: refactor and use okHttpClient instead! private String downloadWithoutCA(String url_string) { String string = ""; try { @@ -869,11 +997,11 @@ public class ProviderAPI extends IntentService { System.out.println("String ignoring certificate = " + string); } catch (FileNotFoundException e) { e.printStackTrace(); - string = formatErrorMessage(R.string.malformed_url); + string = formatErrorMessage(malformed_url); } catch (IOException e) { // The downloaded certificate doesn't validate our https connection. e.printStackTrace(); - string = formatErrorMessage(R.string.certificate_error); + string = formatErrorMessage(certificate_error); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); @@ -889,6 +1017,7 @@ public class ProviderAPI extends IntentService { * * @return true if there were no exceptions */ + //FIXME: refactor and use okHttpClient instead! private boolean logOut() { String delete_url = provider_api_url + "/logout"; diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 67fd0a1a..d751d9f0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,10 +27,11 @@ android:maxSdkVersion="18"/> from okttp3 source code example + TrustManager[] trustManagers = tmf.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + + trustManager = trustManagers[0]; + + // Create an SSLContext that uses our TrustManager + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, tmf.getTrustManagers(), null); + + } + + + @Override + public String[] getDefaultCipherSuites() { + return internalSSLSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return internalSSLSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableTLSOnSocket(Socket socket) { + if(socket != null && (socket instanceof SSLSocket)) { + ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); + ((SSLSocket)socket).setEnabledCipherSuites(getSupportedCipherSuites()); + } + return socket; + + + } + + + +} diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java index 7dbbe059..88dec39b 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java @@ -23,10 +23,13 @@ import android.view.*; import android.widget.*; import butterknife.*; +import se.leap.bitmaskclient.ProviderAPI; import se.leap.bitmaskclient.VpnFragment; import se.leap.bitmaskclient.Provider; import se.leap.bitmaskclient.R; +import static android.view.View.VISIBLE; + /** * Implements the log in dialog, currently without progress dialog. *

@@ -47,7 +50,8 @@ public class SessionDialog extends DialogFragment { public static enum ERRORS { USERNAME_MISSING, PASSWORD_INVALID_LENGTH, - RISEUP_WARNING + RISEUP_WARNING, + INITIALIZATION_ERROR } @InjectView(R.id.user_message) @@ -117,8 +121,11 @@ public class SessionDialog extends DialogFragment { if (arguments.containsKey(ERRORS.PASSWORD_INVALID_LENGTH.toString())) password_field.setError(getString(R.string.error_not_valid_password_user_message)); else if (arguments.containsKey(ERRORS.RISEUP_WARNING.toString())) { - user_message.setVisibility(TextView.VISIBLE); + user_message.setVisibility(VISIBLE); user_message.setText(R.string.login_riseup_warning); + } else if (arguments.containsKey(ERRORS.INITIALIZATION_ERROR.toString())) { + user_message.setVisibility(VISIBLE); + user_message.setText(String.valueOf(arguments.get(ERRORS.INITIALIZATION_ERROR.toString()))); } if (arguments.containsKey(USERNAME)) { String username = arguments.getString(USERNAME); @@ -129,8 +136,8 @@ public class SessionDialog extends DialogFragment { } if (arguments.containsKey(getString(R.string.user_message))) { user_message.setText(arguments.getString(getString(R.string.user_message))); - user_message.setVisibility(View.VISIBLE); - } else if (user_message.getVisibility() != TextView.VISIBLE) + user_message.setVisibility(VISIBLE); + } else if (user_message.getVisibility() != VISIBLE) user_message.setVisibility(View.GONE); if (!username_field.getText().toString().isEmpty() && password_field.isFocusable()) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7f0670b8..09bac1ef 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -38,7 +38,7 @@ Inténtalo de nuevo: error en el cliente HTTP Inténtalo de nuevo: error de E/S Inténtalo de nuevo: respuesta mal formada del servidor - Actualiza Bitmask + Algoritmo de cifrado no encontrado. Por favor actualice su sistema operativo! Registrarse/Iniciar sesión Iniciar sesión Cerrar sesión diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06c80f12..b1fce0ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ Try again: Client HTTP error Try again: I/O error Try again: Bad response from the server - Update the app + Encryption algorithm not found. Please update your OS! Sign Up/Log In Log In Log Out -- cgit v1.2.3 From 68bc106ee872b13830dfa5fa9794f7cecb306d8e Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sat, 28 Oct 2017 20:41:05 +0200 Subject: #8757 refactores ProviderAPI for insecure flavor, fixes tests, renames confusing constants, updates robotium --- app/build.gradle | 2 +- .../java/se/leap/bitmaskclient/test/TestEIP.java | 4 +- .../leap/bitmaskclient/test/TestVpnFragment.java | 2 +- .../test/UserStatusTestController.java | 3 +- .../leap/bitmaskclient/test/VpnTestController.java | 2 +- .../java/se/leap/bitmaskclient/ProviderAPI.java | 419 ++++++++++----------- .../main/java/se/leap/bitmaskclient/Dashboard.java | 4 +- .../leap/bitmaskclient/TLSCompatSocketFactory.java | 60 ++- .../java/se/leap/bitmaskclient/VpnFragment.java | 7 +- .../java/se/leap/bitmaskclient/eip/Constants.java | 2 +- .../main/java/se/leap/bitmaskclient/eip/EIP.java | 2 +- .../se/leap/bitmaskclient/eip/GatewaysManager.java | 2 +- .../leap/bitmaskclient/eip/VpnConfigGenerator.java | 2 +- .../bitmaskclient/eip/GatewaysManagerTest.java | 2 +- 14 files changed, 260 insertions(+), 253 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7f838cdf..7a2fa73a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -51,7 +51,7 @@ android { dependencies { testCompile 'org.mockito:mockito-core:2.6.3' - androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.5.4' + androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.6.3' testCompile 'junit:junit:4.12' testCompile 'org.json:json:20170516' compile 'com.jakewharton:butterknife:6.1.0' diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java index 1a32e1c9..15bb53e1 100644 --- a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestEIP.java @@ -59,13 +59,13 @@ public class TestEIP extends ServiceTestCase { } private void testEmptyCertificate() { - preferences.edit().putString(Constants.CERTIFICATE, "").apply(); + preferences.edit().putString(Constants.VPN_CERTIFICATE, "").apply(); startService(Constants.ACTION_CHECK_CERT_VALIDITY); } private void testExpiredCertificate() { String expired_certificate = "expired certificate"; - preferences.edit().putString(Constants.CERTIFICATE, expired_certificate).apply(); + preferences.edit().putString(Constants.VPN_CERTIFICATE, expired_certificate).apply(); startService(Constants.ACTION_CHECK_CERT_VALIDITY); } diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestVpnFragment.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestVpnFragment.java index b16bcf4f..564e251c 100644 --- a/app/src/androidTest/java/se/leap/bitmaskclient/test/TestVpnFragment.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/TestVpnFragment.java @@ -60,7 +60,7 @@ public class TestVpnFragment extends BaseTestDashboardFragment { private void checkProvider(String provider) { changeProviderAndLogIn(provider); vpn_controller.sleepSeconds(1); - vpn_controller.turnVpnOndAndOff(); + vpn_controller.turnVpnOnAndOff(); vpn_controller.sleepSeconds(1); } diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/UserStatusTestController.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/UserStatusTestController.java index 76d14911..f5cb2260 100644 --- a/app/src/androidTest/java/se/leap/bitmaskclient/test/UserStatusTestController.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/UserStatusTestController.java @@ -30,7 +30,8 @@ public class UserStatusTestController { void logIn(String username, String password, boolean expectSuccess) { solo.enterText(0, username); solo.enterText(1, password); - solo.clickOnButton(solo.getString(R.string.login_button)); + // https://stackoverflow.com/questions/33560746/how-to-test-alertdialog-item-click-in-robotium-for-android-studio + solo.clickOnView(solo.getView(android.R.id.button1)); assertTrue(solo.waitForDialogToClose()); if (expectSuccess) { diff --git a/app/src/androidTest/java/se/leap/bitmaskclient/test/VpnTestController.java b/app/src/androidTest/java/se/leap/bitmaskclient/test/VpnTestController.java index 93b1da47..f76ba93b 100644 --- a/app/src/androidTest/java/se/leap/bitmaskclient/test/VpnTestController.java +++ b/app/src/androidTest/java/se/leap/bitmaskclient/test/VpnTestController.java @@ -22,7 +22,7 @@ public class VpnTestController { this.solo = solo; } - protected void turnVpnOndAndOff() { + protected void turnVpnOnAndOff() { clickVpnButton(); turningEipOn(); clickVpnButton(); diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index 4805456c..52ca89eb 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -25,6 +25,8 @@ import android.os.Bundle; import android.os.ResultReceiver; import android.support.annotation.NonNull; import android.util.Base64; +import android.util.Log; +import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; @@ -35,15 +37,12 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.ConnectException; -import java.net.CookieHandler; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; -import java.net.URLConnection; import java.net.UnknownHostException; import java.net.UnknownServiceException; import java.security.KeyManagementException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -54,11 +53,9 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPrivateKey; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.NoSuchElementException; import java.util.Scanner; import javax.net.ssl.HostnameVerifier; @@ -67,9 +64,7 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSession; -import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import okhttp3.CipherSuite; @@ -106,6 +101,7 @@ import static se.leap.bitmaskclient.R.string.service_is_down_error; * * @author parmegv * @author MeanderingCode + * @author cyberta */ public class ProviderAPI extends IntentService { @@ -121,6 +117,7 @@ public class ProviderAPI extends IntentService { RESULT_KEY = "result", RECEIVER_KEY = "receiver", ERRORS = "errors", + ERROR = "error", UPDATE_PROGRESSBAR = "update_progressbar", CURRENT_PROGRESS = "current_progress", DOWNLOAD_EIP_SERVICE = TAG + ".DOWNLOAD_EIP_SERVICE"; @@ -137,8 +134,7 @@ public class ProviderAPI extends IntentService { PROVIDER_OK = 11, PROVIDER_NOK = 12, CORRECTLY_DOWNLOADED_EIP_SERVICE = 13, - INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14, - INITIALIZATION_ERROR = 15; + INCORRECTLY_DOWNLOADED_EIP_SERVICE = 14; private static boolean CA_CERT_DOWNLOADED = false, @@ -158,8 +154,6 @@ public class ProviderAPI extends IntentService { private final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); - private OkHttpClient okHttpClient; - private String initializationError = null; public ProviderAPI() { super(TAG); @@ -171,7 +165,6 @@ public class ProviderAPI extends IntentService { preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); resources = getResources(); - initOkHttpClient(); } public static String lastProviderMainUrl() { @@ -187,14 +180,6 @@ public class ProviderAPI extends IntentService { protected void onHandleIntent(Intent command) { final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); - if (initializationError != null) { - Bundle result = new Bundle(); - result.putString(SessionDialog.ERRORS.INITIALIZATION_ERROR.toString(), initializationError); - result.putBoolean(RESULT_KEY, false); - receiver.send(INITIALIZATION_ERROR, result); - return; - } - String action = command.getAction(); Bundle parameters = command.getBundleExtra(PARAMETERS); if (provider_api_url == null && preferences.contains(Provider.KEY)) { @@ -259,7 +244,11 @@ public class ProviderAPI extends IntentService { } private String formatErrorMessage(final int toastStringId) { - return "{ \"" + ERRORS + "\" : \"" + getResources().getString(toastStringId) + "\" }"; + return formatErrorMessage(getResources().getString(toastStringId)); + } + + private String formatErrorMessage(String errorMessage) { + return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; } private JSONObject getErrorMessageAsJson(final int toastStringId) { @@ -271,44 +260,62 @@ public class ProviderAPI extends IntentService { } } - private void initOkHttpClient() { + private JSONObject getErrorMessageAsJson(String message) { try { + return new JSONObject(formatErrorMessage(message)); + } 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(); - initSSLSocketFactory(clientBuilder); + if (isSelfSigned) { + sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, "")); + + } else { + sslCompatFactory = new TLSCompatSocketFactory(); + } + sslCompatFactory.initSSLSocketFactory(clientBuilder); clientBuilder.cookieJar(getCookieJar()) .connectionSpecs(Collections.singletonList(spec)); - okHttpClient = clientBuilder.build(); - + return clientBuilder.build(); } catch (IllegalStateException e) { e.printStackTrace(); - //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); - initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + initError = getErrorMessageAsJson(String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); } catch (KeyStoreException e) { e.printStackTrace(); - //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); - initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + initError = getErrorMessageAsJson(String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); } catch (KeyManagementException e) { e.printStackTrace(); - //initializationError = String.format(formatErrorMessage(keyChainAccessError), e.getLocalizedMessage()); - initializationError = String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage()); + initError = getErrorMessageAsJson(String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); - //initializationError = formatErrorMessage(error_no_such_algorithm_exception_user_message); - initializationError = getResources().getString(error_no_such_algorithm_exception_user_message); + initError = getErrorMessageAsJson(getResources().getString(error_no_such_algorithm_exception_user_message)); } catch (CertificateException e) { e.printStackTrace(); - initializationError = getResources().getString(certificate_error); + initError = getErrorMessageAsJson(getResources().getString(certificate_error)); } catch (UnknownHostException e) { e.printStackTrace(); - initializationError = getResources().getString(server_unreachable_message); + initError = getErrorMessageAsJson(getResources().getString(server_unreachable_message)); } catch (IOException e) { e.printStackTrace(); - initializationError = getResources().getString(error_io_exception_user_message); + initError = getErrorMessageAsJson(getResources().getString(error_io_exception_user_message)); } catch (NoSuchProviderException e) { e.printStackTrace(); } + return null; + } + private OkHttpClient initCommercialCAHttpClient(JSONObject initError) { + return initHttpClient(initError, false); + } + + private OkHttpClient initSelfSignedCAHttpClient(JSONObject initError) { + return initHttpClient(initError, true); } @NonNull @@ -374,16 +381,22 @@ public class ProviderAPI extends IntentService { } private Bundle register(String username, String password) { + JSONObject stepResult = null; + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + LeapSRPSession client = new LeapSRPSession(username, password); byte[] salt = client.calculateNewSalt(); BigInteger password_verifier = client.calculateV(username, password, salt); - JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16)); + stepResult = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); Bundle result = new Bundle(); - if (api_result.has(ERRORS)) - result = authFailedNotification(api_result, username); + if (stepResult.has(ERRORS)) + result = authFailedNotification(stepResult, username); else { result.putString(SessionDialog.USERNAME, username); result.putString(SessionDialog.PASSWORD, password); @@ -426,23 +439,28 @@ public class ProviderAPI extends IntentService { private Bundle authenticate(String username, String password) { Bundle result = new Bundle(); + JSONObject stepResult = new JSONObject(); + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } LeapSRPSession client = new LeapSRPSession(username, password); byte[] A = client.exponential(); - JSONObject step_result = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16)); + stepResult = sendAToSRPServer(provider_api_url, 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(); + String salt = stepResult.getString(LeapSRPSession.SALT); + byte[] Bbytes = new BigInteger(stepResult.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); - setTokenIfAvailable(step_result); - byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); + stepResult = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient); + setTokenIfAvailable(stepResult); + byte[] M2 = new BigInteger(stepResult.getString(LeapSRPSession.M2), 16).toByteArray(); if (client.verify(M2)) { result.putBoolean(RESULT_KEY, true); } else { - authFailedNotification(step_result, username); + authFailedNotification(stepResult, username); } } else { result.putBoolean(RESULT_KEY, false); @@ -450,7 +468,7 @@ public class ProviderAPI extends IntentService { result.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message)); } } catch (JSONException e) { - result = authFailedNotification(step_result, username); + result = authFailedNotification(stepResult, username); e.printStackTrace(); } @@ -460,8 +478,7 @@ public class ProviderAPI extends IntentService { private boolean setTokenIfAvailable(JSONObject authentication_step_result) { try { LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - CookieHandler.setDefault(null); // we don't need cookies anymore - } catch (JSONException e) { // + } catch (JSONException e) { return false; } return true; @@ -542,11 +559,12 @@ public class ProviderAPI extends IntentService { * @param server_url * @param username * @param clientA First SRP parameter sent + * @param okHttpClient * @return response from authentication server */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA) { + private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { SrpCredentials srpCredentials = new SrpCredentials(username, clientA); - return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString()); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); } /** @@ -555,12 +573,13 @@ public class ProviderAPI extends IntentService { * @param server_url * @param username * @param m1 Second SRP parameter sent + * @param okHttpClient * @return response from authentication server */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1) { + private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json); + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); } /** @@ -572,20 +591,48 @@ public class ProviderAPI extends IntentService { * @param password_verifier * @return response from authentication server */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier) { - return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString()); + private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); } - private JSONObject sendToServer(String url, String request_method, String jsonString) { - Response response; - JSONObject responseJson = new JSONObject(); + private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { + return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); + } + + private String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { + return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); + } + + + + private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + JSONObject responseJson; + String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + + try { + responseJson = new JSONObject(plain_response); + } catch (JSONException e) { + e.printStackTrace(); + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } + return responseJson; - RequestBody jsonBody = RequestBody.create(JSON, jsonString); + } + + private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + Response response; + String plainResponseBody = null; - Request request = new Request.Builder() + RequestBody jsonBody = jsonString != null ? RequestBody.create(JSON, jsonString) : null; + Request.Builder requestBuilder = new Request.Builder() .url(url) - .method(request_method, jsonBody) - .build(); + .method(request_method, jsonBody); + if (headerArgs != null) { + for (Pair keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); + } + } + Request request = requestBuilder.build(); try { response = okHttpClient.newCall(request).execute(); @@ -593,35 +640,33 @@ public class ProviderAPI extends IntentService { InputStream inputStream = response.body().byteStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); if (scanner.hasNext()) { - String plain_response = scanner.next(); - responseJson = new JSONObject(plain_response); + plainResponseBody = scanner.next(); } - } catch (JSONException e) { - responseJson = getErrorMessageAsJson(error_json_exception_user_message); } catch (NullPointerException npe) { - responseJson = getErrorMessageAsJson(error_json_exception_user_message); + plainResponseBody = formatErrorMessage(error_json_exception_user_message); } catch (UnknownHostException e) { - responseJson = getErrorMessageAsJson(server_unreachable_message); + plainResponseBody = formatErrorMessage(server_unreachable_message); } catch (MalformedURLException e) { - responseJson = getErrorMessageAsJson(malformed_url); + plainResponseBody = formatErrorMessage(malformed_url); } catch (SocketTimeoutException e) { - responseJson = getErrorMessageAsJson(server_unreachable_message); + plainResponseBody = formatErrorMessage(server_unreachable_message); } catch (SSLHandshakeException e) { - responseJson = getErrorMessageAsJson(certificate_error); + plainResponseBody = formatErrorMessage(certificate_error); } catch (ConnectException e) { - responseJson = getErrorMessageAsJson(service_is_down_error); + plainResponseBody = formatErrorMessage(service_is_down_error); + } catch (IllegalArgumentException e) { + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); } catch (UnknownServiceException e) { //unable to find acceptable protocols - tlsv1.2 not enabled? - responseJson = getErrorMessageAsJson(error_no_such_algorithm_exception_user_message); + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); } catch (IOException e) { - responseJson = getErrorMessageAsJson(error_io_exception_user_message); + plainResponseBody = formatErrorMessage(error_io_exception_user_message); } - return responseJson; + return plainResponseBody; } - /** * Downloads a provider.json from a given URL, adding a new provider using the given name. * @@ -808,13 +853,13 @@ public class ProviderAPI extends IntentService { return error_message; } - private String downloadWithCommercialCA(String url_string, boolean danger_on, String ca_cert_fingerprint) { + //TODO: refactor with ticket #8773 + private String downloadWithCommercialCA(String urlString, boolean dangerOn, String caCertFingerprint) { String result = ""; - int seconds_of_timeout = 2; - String[] pins = new String[] {ca_cert_fingerprint}; + String[] pins = new String[] {caCertFingerprint}; try { - URL url = new URL(url_string); + URL url = new URL(urlString); HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(getApplicationContext(), pins, url); connection.setConnectTimeout(seconds_of_timeout * 1000); if (!LeapSRPSession.getToken().isEmpty()) @@ -822,7 +867,7 @@ public class ProviderAPI extends IntentService { result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next(); } catch (IOException e) { if(e instanceof SSLHandshakeException) { - result = danger_on ? downloadWithoutCA(url_string) : + result = dangerOn ? downloadWithoutCA(urlString) : formatErrorMessage(R.string.error_security_pinnedcertificate); } else result = formatErrorMessage(error_io_exception_user_message); @@ -841,124 +886,83 @@ public class ProviderAPI extends IntentService { * @return */ private String downloadWithCommercialCA(String string_url, boolean danger_on) { + String responseString; + JSONObject errorJson = new JSONObject(); + + OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson); + if (okHttpClient == null) { + return errorJson.toString(); + } - String json_file_content = ""; + List> headerArgs = getAuthorizationHeader(); - URL provider_url = null; - int seconds_of_timeout = 2; - try { - provider_url = new URL(string_url); - URLConnection url_connection = provider_url.openConnection(); + responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); - url_connection.setConnectTimeout(seconds_of_timeout * 1000); - if (!LeapSRPSession.getToken().isEmpty()) - url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); - json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next(); - } catch (MalformedURLException e) { - json_file_content = formatErrorMessage(malformed_url); - } catch (SocketTimeoutException e) { - json_file_content = formatErrorMessage(server_unreachable_message); - } catch (SSLHandshakeException e) { - if (provider_url != null) { - json_file_content = downloadWithProviderCA(string_url, danger_on); - } else { - json_file_content = formatErrorMessage(certificate_error); - } - } catch (ConnectException e) { - json_file_content = formatErrorMessage(service_is_down_error); - } catch (FileNotFoundException e) { - json_file_content = formatErrorMessage(malformed_url); - } catch (Exception e) { - if (provider_url != null && danger_on) { - json_file_content = downloadWithProviderCA(string_url, danger_on); + if (responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (danger_on && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { + responseString = downloadWithProviderCA(string_url, danger_on); + } + } catch (JSONException e) { + e.printStackTrace(); } } - return json_file_content; + return responseString; + } + + @NonNull + private List> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + headerArgs.add(authorizationHeaderPair); + } + return headerArgs; } /** * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. * - * @param url_string as a string - * @param danger_on true to download CA certificate in case it has not been downloaded. + * @param urlString as a string + * @param dangerOn true to download CA certificate in case it has not been downloaded. * @return an empty string if it fails, the url content if not. */ - //FIXME: refactor and use okHttpClient instead! - private String downloadWithProviderCA(String url_string, boolean danger_on) { - String json_file_content = ""; - - try { - URL url = new URL(url_string); - // Tell the URLConnection to use a SocketFactory from our SSLContext - HttpsURLConnection urlConnection = - (HttpsURLConnection) url.openConnection(); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); - if (!LeapSRPSession.getToken().isEmpty()) - urlConnection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (UnknownHostException e) { - e.printStackTrace(); - json_file_content = formatErrorMessage(server_unreachable_message); - } catch (IOException e) { - // The downloaded certificate doesn't validate our https connection. - if (danger_on) { - json_file_content = downloadWithoutCA(url_string); - } else { - json_file_content = formatErrorMessage(certificate_error); - } - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchElementException e) { - e.printStackTrace(); - json_file_content = formatErrorMessage(server_unreachable_message); + private String downloadWithProviderCA(String urlString, boolean dangerOn) { + Log.d(TAG, "download with providerCA: " + urlString); + JSONObject initError = new JSONObject(); + String responseString; + + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError); + if (okHttpClient == null) { + return initError.toString(); } - return json_file_content; - } - - @Deprecated - private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException { - String provider_cert_string = preferences.getString(Provider.CA_CERT, ""); - - java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(provider_cert_string); - // Create a KeyStore containing our trusted CAs - String keyStoreType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(null, null); - keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); + List> headerArgs = getAuthorizationHeader(); - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - // Create an SSLContext that uses our TrustManager - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, tmf.getTrustManagers(), null); - - return context.getSocketFactory(); - } + if (responseString.contains(ERRORS)) { + try { + // danger danger: try to download without CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (dangerOn && responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { + responseString = downloadWithoutCA(urlString); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } - private void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException, NoSuchProviderException { - TLSCompatSocketFactory sslCompatFactory = new TLSCompatSocketFactory(preferences.getString(Provider.CA_CERT, "")); - sslCompatFactory.initSSLSocketFactory(builder); + return responseString; } /** * Downloads the string that's in the url with any certificate. */ - // FIXME: refactor and use okHttpClient instead! + // This method is totally insecure anyways. So no need to refactor that in order to use okHttpClient, force modern TLS etc.. DO NOT USE IN PRODUCTION! private String downloadWithoutCA(String url_string) { String string = ""; try { @@ -1017,55 +1021,31 @@ public class ProviderAPI extends IntentService { * * @return true if there were no exceptions */ - //FIXME: refactor and use okHttpClient instead! private boolean logOut() { - String delete_url = provider_api_url + "/logout"; - HttpsURLConnection urlConnection = null; - int responseCode = 0; + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); + if (okHttpClient == null) { + return false; + } + + String deleteUrl = provider_api_url + "/logout"; int progress = 0; - try { - urlConnection = (HttpsURLConnection) new URL(delete_url).openConnection(); - urlConnection.setRequestMethod("DELETE"); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + Request.Builder requestBuilder = new Request.Builder() + .url(deleteUrl) + .delete(); + Request request = requestBuilder.build(); - responseCode = urlConnection.getResponseCode(); - broadcastProgress(progress++); - LeapSRPSession.setToken(""); - } catch (IndexOutOfBoundsException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (IOException e) { - // TODO Auto-generated catch block - try { - if (urlConnection != null) { - responseCode = urlConnection.getResponseCode(); - if (responseCode == 401) { - broadcastProgress(progress++); - LeapSRPSession.setToken(""); - return true; - } - } - } catch (IOException e1) { - e1.printStackTrace(); + try { + Response response = okHttpClient.newCall(request).execute(); + // v---- was already not authorized + if (response.isSuccessful() || response.code() == 401) { + broadcastProgress(progress++); + LeapSRPSession.setToken(""); } - e.printStackTrace(); + } catch (IOException e) { return false; - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } return true; } @@ -1080,11 +1060,10 @@ public class ProviderAPI extends IntentService { JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.CERTIFICATE); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); boolean danger_on = preferences.getBoolean(ProviderItem.DANGER_ON, false); - String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), danger_on); if (cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) @@ -1121,7 +1100,7 @@ public class ProviderAPI extends IntentService { X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString); certificateString = Base64.encodeToString(certCert.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); + preferences.edit().putString(Constants.VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); return true; } catch (CertificateException e) { diff --git a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java index 9fc7d593..a47b8767 100644 --- a/app/src/main/java/se/leap/bitmaskclient/Dashboard.java +++ b/app/src/main/java/se/leap/bitmaskclient/Dashboard.java @@ -363,9 +363,7 @@ public class Dashboard extends Activity implements ProviderAPIResultReceiver.Rec @Override public void onReceiveResult(int resultCode, Bundle resultData) { - if (resultCode == ProviderAPI.INITIALIZATION_ERROR) { - sessionDialog(resultData); - } else if (resultCode == ProviderAPI.SUCCESSFUL_SIGNUP) { + if (resultCode == ProviderAPI.SUCCESSFUL_SIGNUP) { String username = resultData.getString(SessionDialog.USERNAME); String password = resultData.getString(SessionDialog.PASSWORD); user_status_fragment.logIn(username, password); diff --git a/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java b/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java index fdad6ba9..76d38447 100644 --- a/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java +++ b/app/src/main/java/se/leap/bitmaskclient/TLSCompatSocketFactory.java @@ -1,6 +1,6 @@ package se.leap.bitmaskclient; -import android.util.Log; +import android.text.TextUtils; import java.io.IOException; import java.net.InetAddress; @@ -33,14 +33,14 @@ public class TLSCompatSocketFactory extends SSLSocketFactory { private static final String TAG = TLSCompatSocketFactory.class.getName(); private SSLSocketFactory internalSSLSocketFactory; - private SSLContext sslContext; private TrustManager trustManager; public TLSCompatSocketFactory(String trustedCaCert) throws KeyManagementException, NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException, NoSuchProviderException { + initForSelfSignedCAs(trustedCaCert); + } - initTrustManager(trustedCaCert); - internalSSLSocketFactory = sslContext.getSocketFactory(); - + public TLSCompatSocketFactory() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException, NoSuchProviderException, IOException { + initForCommercialCAs(); } public void initSSLSocketFactory(OkHttpClient.Builder builder) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException, IllegalStateException { @@ -48,14 +48,15 @@ public class TLSCompatSocketFactory extends SSLSocketFactory { } - private void initTrustManager(String trustedCaCert) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, IllegalStateException, KeyManagementException, NoSuchProviderException { - java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(trustedCaCert); - + private void initForSelfSignedCAs(String trustedSelfSignedCaCert) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, IllegalStateException, KeyManagementException, NoSuchProviderException { // Create a KeyStore containing our trusted CAs String defaultType = KeyStore.getDefaultType(); KeyStore keyStore = KeyStore.getInstance(defaultType); keyStore.load(null, null); - keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); + if (!TextUtils.isEmpty(trustedSelfSignedCaCert)) { + java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(trustedSelfSignedCaCert); + keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); + } // Create a TrustManager that trusts the CAs in our KeyStore String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); @@ -72,9 +73,32 @@ public class TLSCompatSocketFactory extends SSLSocketFactory { trustManager = trustManagers[0]; // Create an SSLContext that uses our TrustManager - sslContext = SSLContext.getInstance("TLS"); + SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, tmf.getTrustManagers(), null); + internalSSLSocketFactory = sslContext.getSocketFactory(); + + } + + + private void initForCommercialCAs() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { + + // Create a TrustManager that trusts the CAs in our KeyStore + String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); + tmf.init((KeyStore) null); + + // Check if there's only 1 X509Trustmanager -> from okttp3 source code example + TrustManager[] trustManagers = tmf.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + + trustManager = trustManagers[0]; + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + internalSSLSocketFactory = context.getSocketFactory(); } @@ -89,39 +113,39 @@ public class TLSCompatSocketFactory extends SSLSocketFactory { } @Override - public Socket createSocket() throws IOException { + public Socket createSocket() throws IOException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket()); } @Override - public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { + public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose)); } @Override - public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + public Socket createSocket(String host, int port) throws IOException, UnknownHostException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override - public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException { + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort)); } @Override - public Socket createSocket(InetAddress host, int port) throws IOException { + public Socket createSocket(InetAddress host, int port) throws IOException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port)); } @Override - public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException, IllegalArgumentException { return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort)); } - private Socket enableTLSOnSocket(Socket socket) { + private Socket enableTLSOnSocket(Socket socket) throws IllegalArgumentException { if(socket != null && (socket instanceof SSLSocket)) { ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"}); - ((SSLSocket)socket).setEnabledCipherSuites(getSupportedCipherSuites()); + //TODO: add a android version check as soon as a new Android API or bcjsse supports TLSv1.3 } return socket; diff --git a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java index 8cd9fa0f..c85b0151 100644 --- a/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java +++ b/app/src/main/java/se/leap/bitmaskclient/VpnFragment.java @@ -19,6 +19,7 @@ package se.leap.bitmaskclient; import android.app.*; import android.content.*; import android.os.*; +import android.util.Log; import android.view.*; import android.widget.*; @@ -152,11 +153,15 @@ public class VpnFragment extends Fragment implements Observer { Bundle bundle = new Bundle(); bundle.putBoolean(IS_PENDING, true); dashboard.sessionDialog(bundle); + } else { + Log.d(TAG, "WHAT IS GOING ON HERE?!"); + // TODO: implement a fallback: check if vpncertificate was not downloaded properly or give + // a user feedback. A button that does nothing on click is not a good option } } private boolean canStartEIP() { - boolean certificateExists = !Dashboard.preferences.getString(Constants.CERTIFICATE, "").isEmpty(); + boolean certificateExists = !Dashboard.preferences.getString(Constants.VPN_CERTIFICATE, "").isEmpty(); boolean isAllowedAnon = Dashboard.preferences.getBoolean(Constants.ALLOWED_ANON, false); return (isAllowedAnon || certificateExists) && !eip_status.isConnected() && !eip_status.isConnecting(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java b/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java index 39ad7c08..db1cb4a1 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/Constants.java @@ -33,7 +33,7 @@ public interface Constants { public final static String EIP_NOTIFICATION = TAG + ".EIP_NOTIFICATION"; public final static String ALLOWED_ANON = "allow_anonymous"; public final static String ALLOWED_REGISTERED = "allow_registration"; - public final static String CERTIFICATE = "cert"; + public final static String VPN_CERTIFICATE = "cert"; public final static String PRIVATE_KEY = TAG + ".PRIVATE_KEY"; public final static String KEY = TAG + ".KEY"; public final static String RECEIVER_TAG = TAG + ".RECEIVER_TAG"; diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java index 73c7337b..28a9bb50 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/EIP.java @@ -186,7 +186,7 @@ public final class EIP extends IntentService { } private void checkCertValidity() { - VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(CERTIFICATE, "")); + VpnCertificateValidator validator = new VpnCertificateValidator(preferences.getString(VPN_CERTIFICATE, "")); int resultCode = validator.isValid() ? Activity.RESULT_OK : Activity.RESULT_CANCELED; diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java index 6a7e3d0b..177f553e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/GatewaysManager.java @@ -124,7 +124,7 @@ public class GatewaysManager { try { result.put(Provider.CA_CERT, preferences.getString(Provider.CA_CERT, "")); result.put(Constants.PRIVATE_KEY, preferences.getString(Constants.PRIVATE_KEY, "")); - result.put(Constants.CERTIFICATE, preferences.getString(Constants.CERTIFICATE, "")); + result.put(Constants.VPN_CERTIFICATE, preferences.getString(Constants.VPN_CERTIFICATE, "")); } catch (JSONException e) { e.printStackTrace(); } diff --git a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java index 53d81ed3..f428099e 100644 --- a/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java +++ b/app/src/main/java/se/leap/bitmaskclient/eip/VpnConfigGenerator.java @@ -125,7 +125,7 @@ public class VpnConfigGenerator { String openvpn_cert = "" + new_line - + secrets.getString(Constants.CERTIFICATE) + + secrets.getString(Constants.VPN_CERTIFICATE) + new_line + ""; diff --git a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java index e6a67392..833346fa 100644 --- a/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java +++ b/app/src/test/java/se/leap/bitmaskclient/eip/GatewaysManagerTest.java @@ -44,7 +44,7 @@ public class GatewaysManagerTest { when(sharedPreferences.getString(eq(Constants.PRIVATE_KEY), anyString())).thenReturn(secrets.getString(Constants.PRIVATE_KEY)); when(sharedPreferences.getString(eq(Provider.CA_CERT), anyString())).thenReturn(secrets.getString(Provider.CA_CERT)); - when(sharedPreferences.getString(eq(Constants.CERTIFICATE), anyString())).thenReturn(secrets.getString(Constants.CERTIFICATE)); + when(sharedPreferences.getString(eq(Constants.VPN_CERTIFICATE), anyString())).thenReturn(secrets.getString(Constants.VPN_CERTIFICATE)); when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPreferences); -- cgit v1.2.3 From c90cede07c8d89c5148c8a5c1625586bf89a3db9 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sat, 28 Oct 2017 21:05:58 +0200 Subject: #8757 fix missing constant renamings --- app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java index ff6c0d41..4d159488 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java @@ -970,7 +970,7 @@ public class ProviderAPI extends IntentService { JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.CERTIFICATE); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); @@ -1009,7 +1009,7 @@ public class ProviderAPI extends IntentService { X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); + preferences.edit().putString(Constants.VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); return true; } catch (CertificateException e) { // TODO Auto-generated catch block -- cgit v1.2.3 From dc0a4511dfd54135aeb1e87dac55ed5644e5ae6b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sun, 29 Oct 2017 17:08:02 +0100 Subject: #8757 refactores ProviderAPI for production flavor --- .../java/se/leap/bitmaskclient/ProviderAPI.java | 5 +- .../java/se/leap/bitmaskclient/ProviderAPI.java | 615 +++++++++++---------- 2 files changed, 316 insertions(+), 304 deletions(-) diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index 52ca89eb..17e75ce4 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -307,6 +307,7 @@ public class ProviderAPI extends IntentService { initError = getErrorMessageAsJson(getResources().getString(error_io_exception_user_message)); } catch (NoSuchProviderException e) { e.printStackTrace(); + initError = getErrorMessageAsJson(getResources().getString(error_no_such_algorithm_exception_user_message)); } return null; } @@ -632,6 +633,9 @@ public class ProviderAPI extends IntentService { 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 { @@ -931,7 +935,6 @@ public class ProviderAPI extends IntentService { * @return an empty string if it fails, the url content if not. */ private String downloadWithProviderCA(String urlString, boolean dangerOn) { - Log.d(TAG, "download with providerCA: " + urlString); JSONObject initError = new JSONObject(); String responseString; diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java index 4d159488..08748f97 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java @@ -20,63 +20,71 @@ 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; import android.util.Base64; +import android.util.Pair; import org.json.JSONException; import org.json.JSONObject; import org.thoughtcrime.ssl.pinning.util.PinningHelper; -import java.io.DataOutputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.net.ConnectException; -import java.net.CookieHandler; -import java.net.CookieManager; -import java.net.CookiePolicy; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; -import java.net.URLConnection; -import java.net.URLEncoder; import java.net.UnknownHostException; +import java.net.UnknownServiceException; import java.security.KeyManagementException; -import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; +import java.security.NoSuchProviderException; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; 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.Iterator; +import java.util.List; import java.util.Locale; -import java.util.Map; -import java.util.NoSuchElementException; import java.util.Scanner; -import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSession; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; +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.eip.Constants; import se.leap.bitmaskclient.eip.EIP; import se.leap.bitmaskclient.userstatus.SessionDialog; import se.leap.bitmaskclient.userstatus.User; import se.leap.bitmaskclient.userstatus.UserStatus; +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; + /** * Implements HTTP api methods used to manage communications with the provider server. *

@@ -133,6 +141,9 @@ public class ProviderAPI extends IntentService { go_ahead = false; } + private final MediaType JSON + = MediaType.parse("application/json; charset=utf-8"); + public ProviderAPI() { super(TAG); } @@ -142,17 +153,13 @@ public class ProviderAPI extends IntentService { super.onCreate(); preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); - CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ORIGINAL_SERVER)); + resources = getResources(); } public static String lastProviderMainUrl() { return last_provider_main_url; } - private String formatErrorMessage(final int toast_string_id) { - return "{ \"" + ERRORS + "\" : \"" + getResources().getString(toast_string_id) + "\" }"; - } - @Override protected void onHandleIntent(Intent command) { final ResultReceiver receiver = command.getParcelableExtra(RECEIVER_KEY); @@ -221,6 +228,118 @@ public class ProviderAPI extends IntentService { } } + private String formatErrorMessage(final int toastStringId) { + return formatErrorMessage(getResources().getString(toastStringId)); + } + + private String formatErrorMessage(String errorMessage) { + return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; + } + + private JSONObject getErrorMessageAsJson(final int toastStringId) { + try { + return new JSONObject(formatErrorMessage(toastStringId)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + private JSONObject getErrorMessageAsJson(String message) { + try { + return new JSONObject(formatErrorMessage(message)); + } 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) { + e.printStackTrace(); + initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message)); + } + return null; + } + private OkHttpClient initCommercialCAHttpClient(JSONObject initError) { + return initHttpClient(initError, false); + } + + private 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> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + private Bundle tryToRegister(Bundle task) { Bundle result = new Bundle(); int progress = 0; @@ -247,12 +366,18 @@ public class ProviderAPI extends IntentService { } private Bundle register(String username, String password) { + JSONObject stepResult = null; + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + LeapSRPSession client = new LeapSRPSession(username, password); byte[] salt = client.calculateNewSalt(); BigInteger password_verifier = client.calculateV(username, password, salt); - JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16)); + JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); Bundle result = new Bundle(); if (api_result.has(ERRORS)) @@ -298,17 +423,22 @@ public class ProviderAPI extends IntentService { private Bundle authenticate(String username, String password) { Bundle result = new Bundle(); + JSONObject stepResult = new JSONObject(); + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } LeapSRPSession client = new LeapSRPSession(username, password); byte[] A = client.exponential(); - JSONObject step_result = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16)); + JSONObject step_result = sendAToSRPServer(provider_api_url, 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); + step_result = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient); setTokenIfAvailable(step_result); byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); if (client.verify(M2)) { @@ -319,7 +449,7 @@ public class ProviderAPI extends IntentService { } else { result.putBoolean(RESULT_KEY, false); result.putString(SessionDialog.USERNAME, username); - result.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message)); + result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message)); } } catch (JSONException e) { result = authFailedNotification(step_result, username); @@ -332,7 +462,6 @@ public class ProviderAPI extends IntentService { private boolean setTokenIfAvailable(JSONObject authentication_step_result) { try { LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - CookieHandler.setDefault(null); // we don't need cookies anymore } catch (JSONException e) { // return false; } @@ -340,20 +469,33 @@ public class ProviderAPI extends IntentService { } private Bundle authFailedNotification(JSONObject result, String username) { - Bundle user_notification_bundle = new Bundle(); - try { - JSONObject error_message = result.getJSONObject(ERRORS); - String error_type = error_message.keys().next().toString(); - String message = error_message.get(error_type).toString(); - user_notification_bundle.putString(getResources().getString(R.string.user_message), message); - } catch (JSONException e) { + Bundle userNotificationBundle = new Bundle(); + Object baseErrorMessage = result.opt(ERRORS); + if (baseErrorMessage != null) { + if (baseErrorMessage instanceof JSONObject) { + try { + JSONObject errorMessage = result.getJSONObject(ERRORS); + String errorType = errorMessage.keys().next().toString(); + String message = errorMessage.get(errorType).toString(); + userNotificationBundle.putString(resources.getString(R.string.user_message), message); + } catch (JSONException e) { + e.printStackTrace(); + } + } else if (baseErrorMessage instanceof String) { + try { + String errorMessage = result.getString(ERRORS); + userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } } if (!username.isEmpty()) - user_notification_bundle.putString(SessionDialog.USERNAME, username); - user_notification_bundle.putBoolean(RESULT_KEY, false); + userNotificationBundle.putString(SessionDialog.USERNAME, username); + userNotificationBundle.putBoolean(RESULT_KEY, false); - return user_notification_bundle; + return userNotificationBundle; } /** @@ -401,13 +543,12 @@ public class ProviderAPI extends IntentService { * @param server_url * @param username * @param clientA First SRP parameter sent + * @param okHttpClient * @return response from authentication server */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA) { - Map parameters = new HashMap(); - parameters.put("login", username); - parameters.put("A", clientA); - return sendToServer(server_url + "/sessions.json", "POST", parameters); + private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { + SrpCredentials srpCredentials = new SrpCredentials(username, clientA); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); } /** @@ -416,12 +557,12 @@ public class ProviderAPI extends IntentService { * @param server_url * @param username * @param m1 Second SRP parameter sent + * @param okHttpClient * @return response from authentication server */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1) { - Map parameters = new HashMap(); - parameters.put("client_auth", new BigInteger(1, ConfigHelper.trim(m1)).toString(16)); - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", parameters); + private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { + String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); } /** @@ -431,14 +572,11 @@ public class ProviderAPI extends IntentService { * @param username * @param salt * @param password_verifier + * @param okHttpClient * @return response from authentication server */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier) { - Map parameters = new HashMap(); - parameters.put("user[login]", username); - parameters.put("user[password_salt]", salt); - parameters.put("user[password_verifier]", password_verifier); - return sendToServer(server_url + "/users.json", "POST", parameters); + private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); } /** @@ -446,88 +584,81 @@ public class ProviderAPI extends IntentService { * * @param url * @param request_method - * @param parameters * @return response from authentication server */ - private JSONObject sendToServer(String url, String request_method, Map parameters) { - JSONObject json_response; - HttpsURLConnection urlConnection = null; + private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { + return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); + } + + private String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { + return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); + } + + + + private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + JSONObject responseJson; + String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + try { - InputStream is = null; - urlConnection = (HttpsURLConnection) new URL(url).openConnection(); - urlConnection.setRequestMethod(request_method); - String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); - urlConnection.setRequestProperty("Accept-Language", locale); - urlConnection.setChunkedStreamingMode(0); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); - - DataOutputStream writer = new DataOutputStream(urlConnection.getOutputStream()); - writer.writeBytes(formatHttpParameters(parameters)); - writer.close(); - - is = urlConnection.getInputStream(); - String plain_response = new Scanner(is).useDelimiter("\\A").next(); - json_response = new JSONObject(plain_response); - } catch (IOException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); + responseJson = new JSONObject(plain_response); } catch (JSONException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (KeyManagementException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (KeyStoreException e) { - json_response = getErrorMessage(urlConnection); - e.printStackTrace(); - } catch (CertificateException e) { - json_response = getErrorMessage(urlConnection); e.printStackTrace(); + responseJson = getErrorMessageAsJson(error_json_exception_user_message); } + return responseJson; - return json_response; } - private JSONObject getErrorMessage(HttpsURLConnection urlConnection) { - JSONObject error_message = new JSONObject(); - if (urlConnection != null) { - InputStream error_stream = urlConnection.getErrorStream(); - if (error_stream != null) { - String error_response = new Scanner(error_stream).useDelimiter("\\A").next(); - try { - error_message = new JSONObject(error_response); - } catch (JSONException e) { - e.printStackTrace(); - } - urlConnection.disconnect(); + private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + Response response; + 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 keyValPair : headerArgs) { + requestBuilder.addHeader(keyValPair.first, keyValPair.second); } } - return error_message; - } - - private String formatHttpParameters(Map parameters) throws UnsupportedEncodingException { - StringBuilder result = new StringBuilder(); - boolean first = true; + //TODO: move to getHeaderArgs()? + String locale = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry(); + requestBuilder.addHeader("Accept-Language", locale); + Request request = requestBuilder.build(); - Iterator parameter_iterator = parameters.keySet().iterator(); - while (parameter_iterator.hasNext()) { - if (first) - first = false; - else - result.append("&&"); + try { + response = okHttpClient.newCall(request).execute(); - String key = parameter_iterator.next(); - String value = parameters.get(key); + InputStream inputStream = response.body().byteStream(); + Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); + if (scanner.hasNext()) { + plainResponseBody = scanner.next(); + } - result.append(URLEncoder.encode(key, "UTF-8")); - result.append("="); - result.append(URLEncoder.encode(value, "UTF-8")); + } catch (NullPointerException npe) { + plainResponseBody = formatErrorMessage(error_json_exception_user_message); + } catch (UnknownHostException 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) { + plainResponseBody = formatErrorMessage(service_is_down_error); + } catch (IllegalArgumentException e) { + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (IOException e) { + plainResponseBody = formatErrorMessage(error_io_exception_user_message); } - return result.toString(); + return plainResponseBody; } /** @@ -591,7 +722,7 @@ public class ProviderAPI extends IntentService { result.putBoolean(RESULT_KEY, false); } } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(R.string.malformed_url); + String reason_to_fail = formatErrorMessage(malformed_url); result.putString(ERRORS, reason_to_fail); result.putBoolean(RESULT_KEY, false); } @@ -650,7 +781,6 @@ public class ProviderAPI extends IntentService { if (go_ahead) { String provider_dot_json_string; - if(provider_ca_cert_fingerprint.isEmpty()) provider_dot_json_string = downloadWithCommercialCA(provider_main_url + "/provider.json"); else @@ -720,6 +850,7 @@ public class ProviderAPI extends IntentService { return error_message; } + //TODO: refactor with ticket #8773 private String downloadWithCommercialCA(String url_string, String ca_cert_fingerprint) { String result = ""; @@ -736,7 +867,7 @@ public class ProviderAPI extends IntentService { if(e instanceof SSLHandshakeException) result = formatErrorMessage(R.string.error_security_pinnedcertificate); else - result = formatErrorMessage(R.string.error_io_exception_user_message); + result = formatErrorMessage(error_io_exception_user_message); } return result; @@ -750,212 +881,90 @@ public class ProviderAPI extends IntentService { */ private String downloadWithCommercialCA(String string_url) { - String json_file_content = ""; + String responseString; + JSONObject errorJson = new JSONObject(); - URL provider_url = null; - int seconds_of_timeout = 1; - try { - provider_url = new URL(string_url); - URLConnection url_connection = provider_url.openConnection(); - url_connection.setConnectTimeout(seconds_of_timeout * 1000); - if (!LeapSRPSession.getToken().isEmpty()) - url_connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); - json_file_content = new Scanner(url_connection.getInputStream()).useDelimiter("\\A").next(); - } catch (MalformedURLException e) { - json_file_content = formatErrorMessage(R.string.malformed_url); - } catch (SocketTimeoutException e) { - e.printStackTrace(); - json_file_content = formatErrorMessage(R.string.server_unreachable_message); - } catch (SSLHandshakeException e) { - if (provider_url != null) { - json_file_content = downloadWithProviderCA(string_url); - } else { - json_file_content = formatErrorMessage(R.string.certificate_error); - } - } catch (ConnectException e) { - json_file_content = formatErrorMessage(R.string.service_is_down_error); - } catch (FileNotFoundException e) { - json_file_content = formatErrorMessage(R.string.malformed_url); - } catch (Exception e) { - e.printStackTrace(); - if (provider_url != null) { - json_file_content = downloadWithProviderCA(string_url); - } + OkHttpClient okHttpClient = initCommercialCAHttpClient(errorJson); + if (okHttpClient == null) { + return errorJson.toString(); } - return json_file_content; - } + List> headerArgs = getAuthorizationHeader(); - /** - * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. - * - * @param url_string as a string - * @return an empty string if it fails, the url content if not. - */ - private String downloadWithProviderCA(String url_string) { - String json_file_content = ""; + responseString = sendGetStringToServer(string_url, headerArgs, okHttpClient); - try { - URL url = new URL(url_string); - // Tell the URLConnection to use a SocketFactory from our SSLContext - HttpsURLConnection urlConnection = - (HttpsURLConnection) url.openConnection(); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); - if (!LeapSRPSession.getToken().isEmpty()) - urlConnection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - json_file_content = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (UnknownHostException e) { - e.printStackTrace(); - json_file_content = formatErrorMessage(R.string.server_unreachable_message); - } catch (IOException e) { - // The downloaded certificate doesn't validate our https connection. - json_file_content = formatErrorMessage(R.string.certificate_error); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchElementException e) { - e.printStackTrace(); - json_file_content = formatErrorMessage(R.string.server_unreachable_message); + if (responseString.contains(ERRORS)) { + try { + // try to download with provider CA on certificate error + JSONObject responseErrorJson = new JSONObject(responseString); + if (responseErrorJson.getString(ERRORS).equals(getString(R.string.certificate_error))) { + responseString = downloadWithProviderCA(string_url); + } + } catch (JSONException e) { + e.printStackTrace(); + } } - return json_file_content; - } - - private javax.net.ssl.SSLSocketFactory getProviderSSLSocketFactory() throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, KeyManagementException { - String provider_cert_string = preferences.getString(Provider.CA_CERT, ""); - java.security.cert.Certificate provider_certificate = ConfigHelper.parseX509CertificateFromString(provider_cert_string); - - // Create a KeyStore containing our trusted CAs - String keyStoreType = KeyStore.getDefaultType(); - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - keyStore.load(null, null); - keyStore.setCertificateEntry("provider_ca_certificate", provider_certificate); - - // Create a TrustManager that trusts the CAs in our KeyStore - String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm); - tmf.init(keyStore); + return responseString; + } - // Create an SSLContext that uses our TrustManager - SSLContext context = SSLContext.getInstance("TLS"); - context.init(null, tmf.getTrustManagers(), null); - return context.getSocketFactory(); + @NonNull + private List> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + headerArgs.add(authorizationHeaderPair); + } + return headerArgs; } /** - * Downloads the string that's in the url with any certificate. + * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. + * + * @param urlString as a string + * @return an empty string if it fails, the url content if not. */ - private String downloadWithoutCA(String url_string) { - String string = ""; - try { - - HostnameVerifier hostnameVerifier = new HostnameVerifier() { - @Override - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - - class DefaultTrustManager implements X509TrustManager { - - @Override - public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } + private String downloadWithProviderCA(String urlString) { + JSONObject initError = new JSONObject(); + String responseString; - @Override - public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException { - } + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(initError); + if (okHttpClient == null) { + return initError.toString(); + } - @Override - public X509Certificate[] getAcceptedIssuers() { - return null; - } - } + List> headerArgs = getAuthorizationHeader(); - SSLContext context = SSLContext.getInstance("TLS"); - context.init(new KeyManager[0], new TrustManager[]{new DefaultTrustManager()}, new SecureRandom()); + responseString = sendGetStringToServer(urlString, headerArgs, okHttpClient); - URL url = new URL(url_string); - HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); - urlConnection.setSSLSocketFactory(context.getSocketFactory()); - urlConnection.setHostnameVerifier(hostnameVerifier); - string = new Scanner(urlConnection.getInputStream()).useDelimiter("\\A").next(); - System.out.println("String ignoring certificate = " + string); - } catch (FileNotFoundException e) { - e.printStackTrace(); - string = formatErrorMessage(R.string.malformed_url); - } catch (IOException e) { - // The downloaded certificate doesn't validate our https connection. - e.printStackTrace(); - string = formatErrorMessage(R.string.certificate_error); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return string; + return responseString; } private boolean logOut() { - String delete_url = provider_api_url + "/logout"; + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); + if (okHttpClient == null) { + return false; + } - HttpsURLConnection urlConnection = null; - int responseCode = 0; + String deleteUrl = provider_api_url + "/logout"; int progress = 0; - try { - urlConnection = (HttpsURLConnection) new URL(delete_url).openConnection(); - urlConnection.setRequestMethod("DELETE"); - urlConnection.setSSLSocketFactory(getProviderSSLSocketFactory()); + Request.Builder requestBuilder = new Request.Builder() + .url(deleteUrl) + .delete(); + Request request = requestBuilder.build(); - responseCode = urlConnection.getResponseCode(); - broadcastProgress(progress++); - LeapSRPSession.setToken(""); - } catch (IndexOutOfBoundsException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (IOException e) { - // TODO Auto-generated catch block - try { - if (urlConnection != null) { - responseCode = urlConnection.getResponseCode(); - if (responseCode == 401) { - broadcastProgress(progress++); - LeapSRPSession.setToken(""); - return true; - } - } - } catch (IOException e1) { - e1.printStackTrace(); + try { + Response response = okHttpClient.newCall(request).execute(); + // v---- was already not authorized + if (response.isSuccessful() || response.code() == 401) { + broadcastProgress(progress++); + LeapSRPSession.setToken(""); } - e.printStackTrace(); + } catch (IOException e) { return false; - } catch (KeyManagementException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (KeyStoreException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (NoSuchAlgorithmException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); } return true; } -- cgit v1.2.3 From 1f133c139e4f9f26a265b26cbb6f37c2556a5fcc Mon Sep 17 00:00:00 2001 From: cyBerta Date: Sun, 29 Oct 2017 18:18:38 +0100 Subject: add missing import... --- app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index 17e75ce4..670a88f8 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -56,6 +56,7 @@ 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.HostnameVerifier; -- cgit v1.2.3 From 8158397299dc29e6ffa1018b082c41aad37f18d6 Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 31 Oct 2017 15:58:07 +0100 Subject: #8757 new ProviderApiBase includes commonly used code between different implementations of ProviderAPI --- .../java/se/leap/bitmaskclient/ProviderAPI.java | 852 ++------------------ .../se/leap/bitmaskclient/ProviderApiBase.java | 817 ++++++++++++++++++++ .../se/leap/bitmaskclient/ConfigurationWizard.java | 4 +- .../java/se/leap/bitmaskclient/ProviderAPI.java | 860 ++------------------- 4 files changed, 940 insertions(+), 1593 deletions(-) create mode 100644 app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java diff --git a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java index 670a88f8..588ff7e2 100644 --- a/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/insecure/java/se/leap/bitmaskclient/ProviderAPI.java @@ -16,16 +16,7 @@ */ 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; -import android.util.Base64; -import android.util.Log; import android.util.Pair; import org.json.JSONException; @@ -34,29 +25,13 @@ import org.thoughtcrime.ssl.pinning.util.PinningHelper; import java.io.FileNotFoundException; 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.URL; -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.SecureRandom; -import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; 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.HostnameVerifier; @@ -68,617 +43,42 @@ import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; -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.ProviderListContent.ProviderItem; import se.leap.bitmaskclient.eip.Constants; import se.leap.bitmaskclient.eip.EIP; -import se.leap.bitmaskclient.userstatus.SessionDialog; -import se.leap.bitmaskclient.userstatus.User; -import se.leap.bitmaskclient.userstatus.UserStatus; 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; /** * Implements HTTP api methods used to manage communications with the provider server. + * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors + * of ProviderAPI. *

- * It's an IntentService because it downloads data from the Internet, so it operates in the background. + * It extends 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 { +public class ProviderAPI extends ProviderApiBase { - 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", - ERROR = "error", - 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; - - private static boolean - CA_CERT_DOWNLOADED = false, - PROVIDER_JSON_DOWNLOADED = false, - EIP_SERVICE_JSON_DOWNLOADED = false; - - private static String last_provider_main_url; - private static boolean last_danger_on = false; - private static boolean go_ahead = true; - private static SharedPreferences preferences; - private static String provider_api_url; - private static String provider_ca_cert_fingerprint; - private Resources resources; - public static void stop() { - go_ahead = false; - } - - private final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - - public ProviderAPI() { - super(TAG); - } - - @Override - public void onCreate() { - super.onCreate(); - - preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); - resources = getResources(); - } - - public static String lastProviderMainUrl() { - return last_provider_main_url; - } + private static boolean last_danger_on = true; public static boolean lastDangerOn() { return last_danger_on; } - - @Override - protected void onHandleIntent(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)) { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "no provider")); - provider_api_url = 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)) { - Bundle result = setUpProvider(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(PROVIDER_OK, result); - } else { - receiver.send(PROVIDER_NOK, result); - } - } else if (action.equalsIgnoreCase(SIGN_UP)) { - UserStatus.updateStatus(UserStatus.SessionStatus.SIGNING_UP, resources); - Bundle result = tryToRegister(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_SIGNUP, result); - } else { - receiver.send(FAILED_SIGNUP, result); - } - } else if (action.equalsIgnoreCase(LOG_IN)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_IN, resources); - Bundle result = tryToAuthenticate(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_IN, resources); - } else { - receiver.send(FAILED_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.NOT_LOGGED_IN, resources); - } - } else if (action.equalsIgnoreCase(LOG_OUT)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_OUT, resources); - if (logOut()) { - receiver.send(SUCCESSFUL_LOGOUT, Bundle.EMPTY); - android.util.Log.d(TAG, "Logged out, notifying user status"); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_OUT, resources); - } else { - receiver.send(LOGOUT_FAILED, Bundle.EMPTY); - UserStatus.updateStatus(UserStatus.SessionStatus.DIDNT_LOG_OUT, resources); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { - if (updateVpnCertificate()) { - receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_EIP_SERVICE)) { - Bundle result = getAndSetEipServiceJson(); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(CORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } - } - } - - private String formatErrorMessage(final int toastStringId) { - return formatErrorMessage(getResources().getString(toastStringId)); - } - - private String formatErrorMessage(String errorMessage) { - return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; - } - - private JSONObject getErrorMessageAsJson(final int toastStringId) { - try { - return new JSONObject(formatErrorMessage(toastStringId)); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - private JSONObject getErrorMessageAsJson(String message) { - try { - return new JSONObject(formatErrorMessage(message)); - } 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(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (KeyStoreException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (KeyManagementException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(String.format(getResources().getString(keyChainAccessError), e.getLocalizedMessage())); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(getResources().getString(error_no_such_algorithm_exception_user_message)); - } catch (CertificateException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(getResources().getString(certificate_error)); - } catch (UnknownHostException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(getResources().getString(server_unreachable_message)); - } catch (IOException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(getResources().getString(error_io_exception_user_message)); - } catch (NoSuchProviderException e) { - e.printStackTrace(); - initError = getErrorMessageAsJson(getResources().getString(error_no_such_algorithm_exception_user_message)); - } - return null; - } - private OkHttpClient initCommercialCAHttpClient(JSONObject initError) { - return initHttpClient(initError, false); - } - - private 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> cookieStore = new HashMap<>(); - - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - cookieStore.put(url.host(), cookies); - } - - @Override - public List loadForRequest(HttpUrl url) { - List cookies = cookieStore.get(url.host()); - return cookies != null ? cookies : new ArrayList(); - } - }; - } - - - private Bundle tryToRegister(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - - if (validUserLoginData(username, password)) { - result = register(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle register(String username, String password) { - JSONObject stepResult = null; - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] salt = client.calculateNewSalt(); - - BigInteger password_verifier = client.calculateV(username, password, salt); - - stepResult = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); - - Bundle result = new Bundle(); - if (stepResult.has(ERRORS)) - result = authFailedNotification(stepResult, username); - else { - result.putString(SessionDialog.USERNAME, username); - result.putString(SessionDialog.PASSWORD, password); - result.putBoolean(RESULT_KEY, true); - } - - return result; - } - - /** - * Starts the authentication process using SRP protocol. - * - * @param task containing: username, password and api url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. - */ - private Bundle tryToAuthenticate(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - if (validUserLoginData(username, password)) { - result = authenticate(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - - private Bundle authenticate(String username, String password) { - Bundle result = new Bundle(); - JSONObject stepResult = new JSONObject(); - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] A = client.exponential(); - - stepResult = sendAToSRPServer(provider_api_url, username, new BigInteger(1, A).toString(16), okHttpClient); - try { - String salt = stepResult.getString(LeapSRPSession.SALT); - byte[] Bbytes = new BigInteger(stepResult.getString("B"), 16).toByteArray(); - byte[] M1 = client.response(new BigInteger(salt, 16).toByteArray(), Bbytes); - if (M1 != null) { - stepResult = sendM1ToSRPServer(provider_api_url, username, M1, okHttpClient); - setTokenIfAvailable(stepResult); - byte[] M2 = new BigInteger(stepResult.getString(LeapSRPSession.M2), 16).toByteArray(); - if (client.verify(M2)) { - result.putBoolean(RESULT_KEY, true); - } else { - authFailedNotification(stepResult, username); - } - } else { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putString(getResources().getString(R.string.user_message), getResources().getString(R.string.error_srp_math_error_user_message)); - } - } catch (JSONException e) { - result = authFailedNotification(stepResult, username); - e.printStackTrace(); - } - - return result; - } - - private boolean setTokenIfAvailable(JSONObject authentication_step_result) { - try { - LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - } catch (JSONException e) { - return false; - } - return true; - } - - private Bundle authFailedNotification(JSONObject result, String username) { - Bundle userNotificationBundle = new Bundle(); - Object baseErrorMessage = result.opt(ERRORS); - if (baseErrorMessage != null) { - if (baseErrorMessage instanceof JSONObject) { - try { - JSONObject errorMessage = result.getJSONObject(ERRORS); - String errorType = errorMessage.keys().next().toString(); - String message = errorMessage.get(errorType).toString(); - userNotificationBundle.putString(getResources().getString(R.string.user_message), message); - } catch (JSONException e) { - e.printStackTrace(); - } - } else if (baseErrorMessage instanceof String) { - try { - String errorMessage = result.getString(ERRORS); - userNotificationBundle.putString(getResources().getString(R.string.user_message), errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - - if (!username.isEmpty()) - userNotificationBundle.putString(SessionDialog.USERNAME, username); - userNotificationBundle.putBoolean(RESULT_KEY, false); - - return userNotificationBundle; - } - - /** - * Sets up an intent with the progress value passed as a parameter - * and sends it as a broadcast. - * - * @param progress - */ - private void broadcastProgress(int progress) { - Intent intentUpdate = new Intent(); - intentUpdate.setAction(UPDATE_PROGRESSBAR); - intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); - intentUpdate.putExtra(CURRENT_PROGRESS, progress); - sendBroadcast(intentUpdate); - } - - /** - * Validates parameters entered by the user to log in - * - * @param username - * @param password - * @return true if both parameters are present and the entered password length is greater or equal to eight (8). - */ - private boolean validUserLoginData(String username, String password) { - return validUsername(username) && wellFormedPassword(password); - } - - private boolean validUsername(String username) { - return username != null && !username.isEmpty(); - } - - /** - * Validates a password - * - * @param password - * @return true if the entered password length is greater or equal to eight (8). - */ - private boolean wellFormedPassword(String password) { - return password != null && password.length() >= 8; - } - - /** - * Sends an HTTP POST request to the authentication server with the SRP Parameter A. - * - * @param server_url - * @param username - * @param clientA First SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { - SrpCredentials srpCredentials = new SrpCredentials(username, clientA); - return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); - } - - /** - * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). - * - * @param server_url - * @param username - * @param m1 Second SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { - - String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); - } - - /** - * Sends an HTTP POST request to the api server to register a new user. - * - * @param server_url - * @param username - * @param salt - * @param password_verifier - * @return response from authentication server - */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { - return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); - } - - private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { - return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); - } - - private String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { - return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); - } - - - - private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - JSONObject responseJson; - String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - - try { - responseJson = new JSONObject(plain_response); - } catch (JSONException e) { - e.printStackTrace(); - responseJson = getErrorMessageAsJson(error_json_exception_user_message); - } - return responseJson; - - } - - private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - Response response; - 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 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(); - } - - } catch (NullPointerException npe) { - plainResponseBody = formatErrorMessage(error_json_exception_user_message); - } catch (UnknownHostException 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) { - plainResponseBody = formatErrorMessage(service_is_down_error); - } catch (IllegalArgumentException e) { - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (UnknownServiceException e) { - //unable to find acceptable protocols - tlsv1.2 not enabled? - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (IOException e) { - plainResponseBody = formatErrorMessage(error_io_exception_user_message); - } - - return plainResponseBody; - } - /** * Downloads a provider.json from a given URL, adding a new provider using the given name. * * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. */ - private Bundle setUpProvider(Bundle task) { + @Override + protected Bundle setUpProvider(Bundle task) { int progress = 0; Bundle current_download = new Bundle(); @@ -715,75 +115,6 @@ public class ProviderAPI extends IntentService { return current_download; } - private Bundle downloadCACert(boolean danger_on) { - Bundle result = new Bundle(); - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); - String cert_string = downloadWithCommercialCA(ca_cert_url, danger_on); - - if (validCertificate(cert_string) && go_ahead) { - preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); - result.putBoolean(RESULT_KEY, true); - } else { - String reason_to_fail = pickErrorMessage(cert_string); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(malformed_url); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - - return result; - } - - public static boolean caCertDownloaded() { - return CA_CERT_DOWNLOADED; - } - - private boolean validCertificate(String cert_string) { - boolean result = false; - if (!ConfigHelper.checkErroneousDownload(cert_string)) { - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); - try { - if (certificate != null) { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - 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)); - - result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); - } else - result = false; - } catch (JSONException e) { - result = false; - } catch (NoSuchAlgorithmException e) { - result = false; - } catch (CertificateEncodingException e) { - result = false; - } - } - - 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)); - } - return hexData.toString(); - } - private Bundle getAndSetProviderJson(String provider_main_url, boolean danger_on, String provider_ca_cert_fingerprint) { Bundle result = new Bundle(); @@ -815,14 +146,19 @@ public class ProviderAPI extends IntentService { return result; } - private Bundle getAndSetEipServiceJson() { + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + @Override + protected Bundle getAndSetEipServiceJson() { Bundle result = new Bundle(); String eip_service_json_string = ""; if (go_ahead) { try { JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); String eip_service_url = provider_json.getString(Provider.API_URL) + "/" + provider_json.getString(Provider.API_VERSION) + "/" + EIP.SERVICE_API_PATH; - eip_service_json_string = downloadWithProviderCA(eip_service_url, true); + eip_service_json_string = downloadWithProviderCA(eip_service_url, last_danger_on); JSONObject eip_service_json = new JSONObject(eip_service_json_string); eip_service_json.getInt(Provider.API_RETURN_SERIAL); @@ -839,23 +175,58 @@ public class ProviderAPI extends IntentService { } /** - * Interprets the error message as a JSON object and extract the "errors" keyword pair. - * If the error message is not a JSON object, then it is returned untouched. + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. * - * @param string_json_error_message - * @return final error message + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. */ - private String pickErrorMessage(String string_json_error_message) { - String error_message = ""; + @Override + protected boolean updateVpnCertificate() { try { - JSONObject json_error_message = new JSONObject(string_json_error_message); - error_message = json_error_message.getString(ERRORS); + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + + String provider_main_url = provider_json.getString(Provider.API_URL); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); + + String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), last_danger_on); + + if (cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) + return false; + else + return loadCertificate(cert_string); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; } catch (JSONException e) { // TODO Auto-generated catch block - error_message = string_json_error_message; + e.printStackTrace(); + return false; } + } + - return error_message; + private Bundle downloadCACert(boolean danger_on) { + Bundle result = new Bundle(); + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); + String cert_string = downloadWithCommercialCA(ca_cert_url, danger_on); + + if (validCertificate(cert_string) && go_ahead) { + preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); + result.putBoolean(RESULT_KEY, true); + } else { + String reason_to_fail = pickErrorMessage(cert_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } catch (JSONException e) { + String reason_to_fail = formatErrorMessage(malformed_url); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + + return result; } //TODO: refactor with ticket #8773 @@ -868,7 +239,7 @@ public class ProviderAPI extends IntentService { HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(getApplicationContext(), pins, url); connection.setConnectTimeout(seconds_of_timeout * 1000); if (!LeapSRPSession.getToken().isEmpty()) - connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); + connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next(); } catch (IOException e) { if(e instanceof SSLHandshakeException) { @@ -918,16 +289,6 @@ public class ProviderAPI extends IntentService { return responseString; } - @NonNull - private List> getAuthorizationHeader() { - List> headerArgs = new ArrayList<>(); - if (!LeapSRPSession.getToken().isEmpty()) { - Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - headerArgs.add(authorizationHeaderPair); - } - return headerArgs; - } - /** * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. * @@ -1020,97 +381,4 @@ public class ProviderAPI extends IntentService { return string; } - /** - * Logs out from the api url retrieved from the task. - * - * @return true if there were no exceptions - */ - private boolean logOut() { - - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); - if (okHttpClient == null) { - return false; - } - - String deleteUrl = provider_api_url + "/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; - } - return true; - } - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json or danger_on flag are not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - private boolean updateVpnCertificate() { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - - String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); - - boolean danger_on = preferences.getBoolean(ProviderItem.DANGER_ON, false); - - String cert_string = downloadWithProviderCA(new_cert_string_url.toString(), danger_on); - - if (cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) - return false; - else - return loadCertificate(cert_string); - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } - - private boolean loadCertificate(String cert_string) { - try { - // API returns concatenated cert & key. Split them for OpenVPN options - String certificateString = null, keyString = null; - String[] certAndKey = cert_string.split("(?<=-\n)"); - for (int i = 0; i < certAndKey.length - 1; i++) { - if (certAndKey[i].contains("KEY")) { - keyString = certAndKey[i++] + certAndKey[i]; - } else if (certAndKey[i].contains("CERTIFICATE")) { - certificateString = certAndKey[i++] + certAndKey[i]; - } - } - RSAPrivateKey keyCert = ConfigHelper.parseRsaKeyFromString(keyString); - keyString = Base64.encodeToString(keyCert.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----").commit(); - - X509Certificate certCert = ConfigHelper.parseX509CertificateFromString(certificateString); - certificateString = Base64.encodeToString(certCert.getEncoded(), Base64.DEFAULT); - - preferences.edit().putString(Constants.VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); - - return true; - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } } diff --git a/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java new file mode 100644 index 00000000..caa48231 --- /dev/null +++ b/app/src/main/java/se/leap/bitmaskclient/ProviderApiBase.java @@ -0,0 +1,817 @@ +/** + * Copyright (c) 2017 LEAP Encryption Access Project and contributers + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package se.leap.bitmaskclient; + +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; +import android.util.Base64; +import android.util.Pair; + +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.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.eip.Constants; +import se.leap.bitmaskclient.userstatus.SessionDialog; +import se.leap.bitmaskclient.userstatus.User; +import se.leap.bitmaskclient.userstatus.UserStatus; + +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; + +/** + * 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. + *

+ * It's an IntentService because it downloads data from the Internet, so it operates in the background. + * + * @author parmegv + * @author MeanderingCode + * @author cyberta + */ + +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 + CA_CERT_DOWNLOADED = false, + PROVIDER_JSON_DOWNLOADED = false, + EIP_SERVICE_JSON_DOWNLOADED = false; + + protected static String last_provider_main_url; + protected static boolean go_ahead = true; + protected static SharedPreferences preferences; + protected static String provider_api_url; + protected static String provider_ca_cert_fingerprint; + protected Resources resources; + + 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(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); + resources = getResources(); + } + + public static String lastProviderMainUrl() { + return last_provider_main_url; + } + + @Override + protected void onHandleIntent(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)) { + 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); + go_ahead = true; + } catch (JSONException e) { + go_ahead = false; + } + } + + if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { + Bundle result = setUpProvider(parameters); + if (go_ahead) { + if (result.getBoolean(RESULT_KEY)) { + receiver.send(PROVIDER_OK, result); + } else { + receiver.send(PROVIDER_NOK, result); + } + } + } else if (action.equalsIgnoreCase(SIGN_UP)) { + UserStatus.updateStatus(UserStatus.SessionStatus.SIGNING_UP, resources); + Bundle result = tryToRegister(parameters); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(SUCCESSFUL_SIGNUP, result); + } else { + receiver.send(FAILED_SIGNUP, result); + } + } else if (action.equalsIgnoreCase(LOG_IN)) { + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_IN, resources); + Bundle result = tryToAuthenticate(parameters); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(SUCCESSFUL_LOGIN, result); + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_IN, resources); + } else { + receiver.send(FAILED_LOGIN, result); + UserStatus.updateStatus(UserStatus.SessionStatus.NOT_LOGGED_IN, resources); + } + } else if (action.equalsIgnoreCase(LOG_OUT)) { + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_OUT, resources); + if (logOut()) { + receiver.send(SUCCESSFUL_LOGOUT, Bundle.EMPTY); + UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_OUT, resources); + } else { + receiver.send(LOGOUT_FAILED, Bundle.EMPTY); + UserStatus.updateStatus(UserStatus.SessionStatus.DIDNT_LOG_OUT, resources); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { + if (updateVpnCertificate()) { + receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); + } + } else if (action.equalsIgnoreCase(DOWNLOAD_EIP_SERVICE)) { + Bundle result = getAndSetEipServiceJson(); + if (result.getBoolean(RESULT_KEY)) { + receiver.send(CORRECTLY_DOWNLOADED_EIP_SERVICE, result); + } else { + receiver.send(INCORRECTLY_DOWNLOADED_EIP_SERVICE, result); + } + } + } + + protected String formatErrorMessage(final int toastStringId) { + return formatErrorMessage(getResources().getString(toastStringId)); + } + + private String formatErrorMessage(String errorMessage) { + return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; + } + + private JSONObject getErrorMessageAsJson(final int toastStringId) { + try { + return new JSONObject(formatErrorMessage(toastStringId)); + } catch (JSONException e) { + e.printStackTrace(); + return new JSONObject(); + } + } + + private JSONObject getErrorMessageAsJson(String message) { + try { + return new JSONObject(formatErrorMessage(message)); + } 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) { + 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> cookieStore = new HashMap<>(); + + @Override + public void saveFromResponse(HttpUrl url, List cookies) { + cookieStore.put(url.host(), cookies); + } + + @Override + public List loadForRequest(HttpUrl url) { + List cookies = cookieStore.get(url.host()); + return cookies != null ? cookies : new ArrayList(); + } + }; + } + + + private Bundle tryToRegister(Bundle task) { + Bundle result = new Bundle(); + int progress = 0; + + String username = User.userName(); + String password = task.getString(SessionDialog.PASSWORD); + + if (validUserLoginData(username, password)) { + result = register(username, password); + broadcastProgress(progress++); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(RESULT_KEY, false); + result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle register(String username, String password) { + JSONObject stepResult = null; + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + + LeapSRPSession client = new LeapSRPSession(username, password); + byte[] salt = client.calculateNewSalt(); + + BigInteger password_verifier = client.calculateV(username, password, salt); + + JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); + + Bundle result = new Bundle(); + if (api_result.has(ERRORS)) + result = authFailedNotification(api_result, username); + else { + result.putString(SessionDialog.USERNAME, username); + result.putString(SessionDialog.PASSWORD, password); + result.putBoolean(RESULT_KEY, true); + } + + return result; + } + + /** + * Starts the authentication process using SRP protocol. + * + * @param task containing: username, password and api url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. + */ + private Bundle tryToAuthenticate(Bundle task) { + Bundle result = new Bundle(); + int progress = 0; + + String username = User.userName(); + String password = task.getString(SessionDialog.PASSWORD); + if (validUserLoginData(username, password)) { + result = authenticate(username, password); + broadcastProgress(progress++); + } else { + if (!wellFormedPassword(password)) { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); + } + if (!validUsername(username)) { + result.putBoolean(RESULT_KEY, false); + result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); + } + } + + return result; + } + + private Bundle authenticate(String username, String password) { + Bundle result = new Bundle(); + JSONObject stepResult = new JSONObject(); + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); + if (okHttpClient == null) { + return authFailedNotification(stepResult, username); + } + + 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); + 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); + setTokenIfAvailable(step_result); + byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); + if (client.verify(M2)) { + result.putBoolean(RESULT_KEY, true); + } else { + authFailedNotification(step_result, username); + } + } else { + result.putBoolean(RESULT_KEY, false); + result.putString(SessionDialog.USERNAME, username); + result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message)); + } + } catch (JSONException e) { + result = authFailedNotification(step_result, username); + e.printStackTrace(); + } + + return result; + } + + private boolean setTokenIfAvailable(JSONObject authentication_step_result) { + try { + LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); + } catch (JSONException e) { // + return false; + } + return true; + } + + private Bundle authFailedNotification(JSONObject result, String username) { + Bundle userNotificationBundle = new Bundle(); + Object baseErrorMessage = result.opt(ERRORS); + if (baseErrorMessage != null) { + if (baseErrorMessage instanceof JSONObject) { + try { + JSONObject errorMessage = result.getJSONObject(ERRORS); + String errorType = errorMessage.keys().next().toString(); + String message = errorMessage.get(errorType).toString(); + userNotificationBundle.putString(resources.getString(R.string.user_message), message); + } catch (JSONException e) { + e.printStackTrace(); + } + } else if (baseErrorMessage instanceof String) { + try { + String errorMessage = result.getString(ERRORS); + userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + if (!username.isEmpty()) + userNotificationBundle.putString(SessionDialog.USERNAME, username); + userNotificationBundle.putBoolean(RESULT_KEY, false); + + return userNotificationBundle; + } + + /** + * Sets up an intent with the progress value passed as a parameter + * and sends it as a broadcast. + * + * @param progress + */ + protected void broadcastProgress(int progress) { + Intent intentUpdate = new Intent(); + intentUpdate.setAction(UPDATE_PROGRESSBAR); + intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); + intentUpdate.putExtra(CURRENT_PROGRESS, progress); + sendBroadcast(intentUpdate); + } + + /** + * Validates parameters entered by the user to log in + * + * @param username + * @param password + * @return true if both parameters are present and the entered password length is greater or equal to eight (8). + */ + private boolean validUserLoginData(String username, String password) { + return validUsername(username) && wellFormedPassword(password); + } + + private boolean validUsername(String username) { + return username != null && !username.isEmpty(); + } + + /** + * Validates a password + * + * @param password + * @return true if the entered password length is greater or equal to eight (8). + */ + private boolean wellFormedPassword(String password) { + return password != null && password.length() >= 8; + } + + /** + * Sends an HTTP POST request to the authentication server with the SRP Parameter A. + * + * @param server_url + * @param username + * @param clientA First SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { + SrpCredentials srpCredentials = new SrpCredentials(username, clientA); + return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); + } + + /** + * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). + * + * @param server_url + * @param username + * @param m1 Second SRP parameter sent + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { + String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; + return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); + } + + /** + * Sends an HTTP POST request to the api server to register a new user. + * + * @param server_url + * @param username + * @param salt + * @param password_verifier + * @param okHttpClient + * @return response from authentication server + */ + private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { + return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); + } + + /** + * Executes an HTTP request expecting a JSON response. + * + * @param url + * @param request_method + * @return response from authentication server + */ + private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { + return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); + } + + protected String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { + return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); + } + + + + private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + JSONObject responseJson; + String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); + + try { + responseJson = new JSONObject(plain_response); + } catch (JSONException e) { + e.printStackTrace(); + responseJson = getErrorMessageAsJson(error_json_exception_user_message); + } + return responseJson; + + } + + private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { + Response response; + 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 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(); + } + + } catch (NullPointerException npe) { + plainResponseBody = formatErrorMessage(error_json_exception_user_message); + } catch (UnknownHostException 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) { + plainResponseBody = formatErrorMessage(service_is_down_error); + } catch (IllegalArgumentException e) { + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (UnknownServiceException e) { + //unable to find acceptable protocols - tlsv1.2 not enabled? + plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); + } catch (IOException e) { + plainResponseBody = formatErrorMessage(error_io_exception_user_message); + } + + return plainResponseBody; + } + + /** + * Downloads a provider.json from a given URL, adding a new provider using the given name. + * + * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. + */ + protected abstract Bundle setUpProvider(Bundle task); + + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + protected abstract Bundle getAndSetEipServiceJson(); + + /** + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. + * + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. + */ + protected abstract boolean updateVpnCertificate(); + + + protected static boolean caCertDownloaded() { + return CA_CERT_DOWNLOADED; + } + + protected boolean validCertificate(String cert_string) { + boolean result = false; + if (!ConfigHelper.checkErroneousDownload(cert_string)) { + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); + try { + if (certificate != null) { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + 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)); + + result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); + } else + result = false; + } catch (JSONException e) { + result = false; + } catch (NoSuchAlgorithmException e) { + result = false; + } catch (CertificateEncodingException e) { + result = false; + } + } + + 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)); + } + return hexData.toString(); + } + + /** + * Interprets the error message as a JSON object and extract the "errors" keyword pair. + * If the error message is not a JSON object, then it is returned untouched. + * + * @param string_json_error_message + * @return final error message + */ + protected String pickErrorMessage(String string_json_error_message) { + String error_message = ""; + try { + JSONObject json_error_message = new JSONObject(string_json_error_message); + error_message = json_error_message.getString(ERRORS); + } catch (JSONException e) { + // TODO Auto-generated catch block + error_message = string_json_error_message; + } + + return error_message; + } + + @NonNull + protected List> getAuthorizationHeader() { + List> headerArgs = new ArrayList<>(); + if (!LeapSRPSession.getToken().isEmpty()) { + Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); + headerArgs.add(authorizationHeaderPair); + } + return headerArgs; + } + + private boolean logOut() { + OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); + if (okHttpClient == null) { + return false; + } + + String deleteUrl = provider_api_url + "/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; + } + return true; + } + + //FIXME: don't save private keys in shared preferences! use the keystore + protected boolean loadCertificate(String cert_string) { + try { + // API returns concatenated cert & key. Split them for OpenVPN options + String certificateString = null, keyString = null; + String[] certAndKey = cert_string.split("(?<=-\n)"); + for (int i = 0; i < certAndKey.length - 1; i++) { + if (certAndKey[i].contains("KEY")) { + keyString = certAndKey[i++] + certAndKey[i]; + } else if (certAndKey[i].contains("CERTIFICATE")) { + certificateString = certAndKey[i++] + certAndKey[i]; + } + } + + RSAPrivateKey key = ConfigHelper.parseRsaKeyFromString(keyString); + keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); + preferences.edit().putString(Constants.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----").commit(); + + X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); + certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); + preferences.edit().putString(Constants.VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); + return true; + } catch (CertificateException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; + } + } +} diff --git a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java index 61c6b7cf..2e5521b1 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java +++ b/app/src/production/java/se/leap/bitmaskclient/ConfigurationWizard.java @@ -348,7 +348,7 @@ public class ConfigurationWizard extends Activity * use it anonymously (if possible) * or cancel his/her election pressing the back button. * - * @param view + * */ private void showProviderDetails() { if (setting_up_provider) { @@ -380,8 +380,6 @@ public class ConfigurationWizard extends Activity /** * Asks ProviderAPI to download a new provider.json file * - * @param provider_name - * @param provider_main_url */ public void setUpProvider() { Intent provider_API_command = new Intent(this, ProviderAPI.class); diff --git a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java index 08748f97..982080de 100644 --- a/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java +++ b/app/src/production/java/se/leap/bitmaskclient/ProviderAPI.java @@ -1,3 +1,4 @@ + /** * Copyright (c) 2013 LEAP Encryption Access Project and contributers * @@ -16,15 +17,7 @@ */ 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; -import android.util.Base64; import android.util.Pair; import org.json.JSONException; @@ -32,634 +25,32 @@ import org.json.JSONObject; import org.thoughtcrime.ssl.pinning.util.PinningHelper; 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.URL; -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.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.HttpsURLConnection; 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.eip.Constants; import se.leap.bitmaskclient.eip.EIP; -import se.leap.bitmaskclient.userstatus.SessionDialog; -import se.leap.bitmaskclient.userstatus.User; -import se.leap.bitmaskclient.userstatus.UserStatus; -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; /** * Implements HTTP api methods used to manage communications with the provider server. + * It extends the abstract ProviderApiBase and implements the diverging method calls between the different flavors + * of ProviderAPI. *

- * It's an IntentService because it downloads data from the Internet, so it operates in the background. + * It extends 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 { - - 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; - - private static boolean - CA_CERT_DOWNLOADED = false, - PROVIDER_JSON_DOWNLOADED = false, - EIP_SERVICE_JSON_DOWNLOADED = false; - - private static String last_provider_main_url; - private static boolean go_ahead = true; - private static SharedPreferences preferences; - private static String provider_api_url; - private static String provider_ca_cert_fingerprint; - private Resources resources; - - public static void stop() { - go_ahead = false; - } - - private final MediaType JSON - = MediaType.parse("application/json; charset=utf-8"); - - public ProviderAPI() { - super(TAG); - } - - @Override - public void onCreate() { - super.onCreate(); - - preferences = getSharedPreferences(Dashboard.SHARED_PREFERENCES, MODE_PRIVATE); - resources = getResources(); - } - - public static String lastProviderMainUrl() { - return last_provider_main_url; - } - - @Override - protected void onHandleIntent(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)) { - 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); - go_ahead = true; - } catch (JSONException e) { - go_ahead = false; - } - } - - if (action.equalsIgnoreCase(SET_UP_PROVIDER)) { - Bundle result = setUpProvider(parameters); - if (go_ahead) { - if (result.getBoolean(RESULT_KEY)) { - receiver.send(PROVIDER_OK, result); - } else { - receiver.send(PROVIDER_NOK, result); - } - } - } else if (action.equalsIgnoreCase(SIGN_UP)) { - UserStatus.updateStatus(UserStatus.SessionStatus.SIGNING_UP, resources); - Bundle result = tryToRegister(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_SIGNUP, result); - } else { - receiver.send(FAILED_SIGNUP, result); - } - } else if (action.equalsIgnoreCase(LOG_IN)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_IN, resources); - Bundle result = tryToAuthenticate(parameters); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(SUCCESSFUL_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_IN, resources); - } else { - receiver.send(FAILED_LOGIN, result); - UserStatus.updateStatus(UserStatus.SessionStatus.NOT_LOGGED_IN, resources); - } - } else if (action.equalsIgnoreCase(LOG_OUT)) { - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGING_OUT, resources); - if (logOut()) { - receiver.send(SUCCESSFUL_LOGOUT, Bundle.EMPTY); - UserStatus.updateStatus(UserStatus.SessionStatus.LOGGED_OUT, resources); - } else { - receiver.send(LOGOUT_FAILED, Bundle.EMPTY); - UserStatus.updateStatus(UserStatus.SessionStatus.DIDNT_LOG_OUT, resources); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_CERTIFICATE)) { - if (updateVpnCertificate()) { - receiver.send(CORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_CERTIFICATE, Bundle.EMPTY); - } - } else if (action.equalsIgnoreCase(DOWNLOAD_EIP_SERVICE)) { - Bundle result = getAndSetEipServiceJson(); - if (result.getBoolean(RESULT_KEY)) { - receiver.send(CORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } else { - receiver.send(INCORRECTLY_DOWNLOADED_EIP_SERVICE, result); - } - } - } - - private String formatErrorMessage(final int toastStringId) { - return formatErrorMessage(getResources().getString(toastStringId)); - } - - private String formatErrorMessage(String errorMessage) { - return "{ \"" + ERRORS + "\" : \"" + errorMessage + "\" }"; - } - - private JSONObject getErrorMessageAsJson(final int toastStringId) { - try { - return new JSONObject(formatErrorMessage(toastStringId)); - } catch (JSONException e) { - e.printStackTrace(); - return new JSONObject(); - } - } - - private JSONObject getErrorMessageAsJson(String message) { - try { - return new JSONObject(formatErrorMessage(message)); - } 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) { - e.printStackTrace(); - initError = getErrorMessageAsJson(resources.getString(error_no_such_algorithm_exception_user_message)); - } - return null; - } - private OkHttpClient initCommercialCAHttpClient(JSONObject initError) { - return initHttpClient(initError, false); - } - - private 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> cookieStore = new HashMap<>(); - - @Override - public void saveFromResponse(HttpUrl url, List cookies) { - cookieStore.put(url.host(), cookies); - } - - @Override - public List loadForRequest(HttpUrl url) { - List cookies = cookieStore.get(url.host()); - return cookies != null ? cookies : new ArrayList(); - } - }; - } - - - private Bundle tryToRegister(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - - if (validUserLoginData(username, password)) { - result = register(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle register(String username, String password) { - JSONObject stepResult = null; - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - LeapSRPSession client = new LeapSRPSession(username, password); - byte[] salt = client.calculateNewSalt(); - - BigInteger password_verifier = client.calculateV(username, password, salt); - - JSONObject api_result = sendNewUserDataToSRPServer(provider_api_url, username, new BigInteger(1, salt).toString(16), password_verifier.toString(16), okHttpClient); - - Bundle result = new Bundle(); - if (api_result.has(ERRORS)) - result = authFailedNotification(api_result, username); - else { - result.putString(SessionDialog.USERNAME, username); - result.putString(SessionDialog.PASSWORD, password); - result.putBoolean(RESULT_KEY, true); - } - - return result; - } - - /** - * Starts the authentication process using SRP protocol. - * - * @param task containing: username, password and api url. - * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if authentication was successful. - */ - private Bundle tryToAuthenticate(Bundle task) { - Bundle result = new Bundle(); - int progress = 0; - - String username = User.userName(); - String password = task.getString(SessionDialog.PASSWORD); - if (validUserLoginData(username, password)) { - result = authenticate(username, password); - broadcastProgress(progress++); - } else { - if (!wellFormedPassword(password)) { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putBoolean(SessionDialog.ERRORS.PASSWORD_INVALID_LENGTH.toString(), true); - } - if (!validUsername(username)) { - result.putBoolean(RESULT_KEY, false); - result.putBoolean(SessionDialog.ERRORS.USERNAME_MISSING.toString(), true); - } - } - - return result; - } - - private Bundle authenticate(String username, String password) { - Bundle result = new Bundle(); - JSONObject stepResult = new JSONObject(); - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(stepResult); - if (okHttpClient == null) { - return authFailedNotification(stepResult, username); - } - - 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); - 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); - setTokenIfAvailable(step_result); - byte[] M2 = new BigInteger(step_result.getString(LeapSRPSession.M2), 16).toByteArray(); - if (client.verify(M2)) { - result.putBoolean(RESULT_KEY, true); - } else { - authFailedNotification(step_result, username); - } - } else { - result.putBoolean(RESULT_KEY, false); - result.putString(SessionDialog.USERNAME, username); - result.putString(resources.getString(R.string.user_message), resources.getString(R.string.error_srp_math_error_user_message)); - } - } catch (JSONException e) { - result = authFailedNotification(step_result, username); - e.printStackTrace(); - } - - return result; - } - - private boolean setTokenIfAvailable(JSONObject authentication_step_result) { - try { - LeapSRPSession.setToken(authentication_step_result.getString(LeapSRPSession.TOKEN)); - } catch (JSONException e) { // - return false; - } - return true; - } - - private Bundle authFailedNotification(JSONObject result, String username) { - Bundle userNotificationBundle = new Bundle(); - Object baseErrorMessage = result.opt(ERRORS); - if (baseErrorMessage != null) { - if (baseErrorMessage instanceof JSONObject) { - try { - JSONObject errorMessage = result.getJSONObject(ERRORS); - String errorType = errorMessage.keys().next().toString(); - String message = errorMessage.get(errorType).toString(); - userNotificationBundle.putString(resources.getString(R.string.user_message), message); - } catch (JSONException e) { - e.printStackTrace(); - } - } else if (baseErrorMessage instanceof String) { - try { - String errorMessage = result.getString(ERRORS); - userNotificationBundle.putString(resources.getString(R.string.user_message), errorMessage); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - - if (!username.isEmpty()) - userNotificationBundle.putString(SessionDialog.USERNAME, username); - userNotificationBundle.putBoolean(RESULT_KEY, false); - - return userNotificationBundle; - } - - /** - * Sets up an intent with the progress value passed as a parameter - * and sends it as a broadcast. - * - * @param progress - */ - private void broadcastProgress(int progress) { - Intent intentUpdate = new Intent(); - intentUpdate.setAction(UPDATE_PROGRESSBAR); - intentUpdate.addCategory(Intent.CATEGORY_DEFAULT); - intentUpdate.putExtra(CURRENT_PROGRESS, progress); - sendBroadcast(intentUpdate); - } - - /** - * Validates parameters entered by the user to log in - * - * @param username - * @param password - * @return true if both parameters are present and the entered password length is greater or equal to eight (8). - */ - private boolean validUserLoginData(String username, String password) { - return validUsername(username) && wellFormedPassword(password); - } - - private boolean validUsername(String username) { - return username != null && !username.isEmpty(); - } - - /** - * Validates a password - * - * @param password - * @return true if the entered password length is greater or equal to eight (8). - */ - private boolean wellFormedPassword(String password) { - return password != null && password.length() >= 8; - } - - /** - * Sends an HTTP POST request to the authentication server with the SRP Parameter A. - * - * @param server_url - * @param username - * @param clientA First SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendAToSRPServer(String server_url, String username, String clientA, OkHttpClient okHttpClient) { - SrpCredentials srpCredentials = new SrpCredentials(username, clientA); - return sendToServer(server_url + "/sessions.json", "POST", srpCredentials.toString(), okHttpClient); - } - - /** - * Sends an HTTP PUT request to the authentication server with the SRP Parameter M1 (or simply M). - * - * @param server_url - * @param username - * @param m1 Second SRP parameter sent - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendM1ToSRPServer(String server_url, String username, byte[] m1, OkHttpClient okHttpClient) { - String m1json = "{\"client_auth\":\"" + new BigInteger(1, ConfigHelper.trim(m1)).toString(16)+ "\"}"; - return sendToServer(server_url + "/sessions/" + username + ".json", "PUT", m1json, okHttpClient); - } - - /** - * Sends an HTTP POST request to the api server to register a new user. - * - * @param server_url - * @param username - * @param salt - * @param password_verifier - * @param okHttpClient - * @return response from authentication server - */ - private JSONObject sendNewUserDataToSRPServer(String server_url, String username, String salt, String password_verifier, OkHttpClient okHttpClient) { - return sendToServer(server_url + "/users.json", "POST", new SrpRegistrationData(username, salt, password_verifier).toString(), okHttpClient); - } - - /** - * Executes an HTTP request expecting a JSON response. - * - * @param url - * @param request_method - * @return response from authentication server - */ - private JSONObject sendToServer(String url, String request_method, String jsonString, OkHttpClient okHttpClient) { - return requestJsonFromServer(url, request_method, jsonString, null, okHttpClient); - } - - private String sendGetStringToServer(String url, List> headerArgs, OkHttpClient okHttpClient) { - return requestStringFromServer(url, "GET", null, headerArgs, okHttpClient); - } - - - - private JSONObject requestJsonFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - JSONObject responseJson; - String plain_response = requestStringFromServer(url, request_method, jsonString, headerArgs, okHttpClient); - - try { - responseJson = new JSONObject(plain_response); - } catch (JSONException e) { - e.printStackTrace(); - responseJson = getErrorMessageAsJson(error_json_exception_user_message); - } - return responseJson; - - } - - private String requestStringFromServer(String url, String request_method, String jsonString, List> headerArgs, @NonNull OkHttpClient okHttpClient) { - Response response; - 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 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(); - } - - } catch (NullPointerException npe) { - plainResponseBody = formatErrorMessage(error_json_exception_user_message); - } catch (UnknownHostException 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) { - plainResponseBody = formatErrorMessage(service_is_down_error); - } catch (IllegalArgumentException e) { - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (UnknownServiceException e) { - //unable to find acceptable protocols - tlsv1.2 not enabled? - plainResponseBody = formatErrorMessage(error_no_such_algorithm_exception_user_message); - } catch (IOException e) { - plainResponseBody = formatErrorMessage(error_io_exception_user_message); - } - - return plainResponseBody; - } +public class ProviderAPI extends ProviderApiBase { /** * Downloads a provider.json from a given URL, adding a new provider using the given name. @@ -667,7 +58,8 @@ public class ProviderAPI extends IntentService { * @param task containing a boolean meaning if the provider is custom or not, another boolean meaning if the user completely trusts this provider, the provider name and its provider.json url. * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the update was successful. */ - private Bundle setUpProvider(Bundle task) { + @Override + protected Bundle setUpProvider(Bundle task) { int progress = 0; Bundle current_download = new Bundle(); @@ -705,77 +97,6 @@ public class ProviderAPI extends IntentService { return current_download; } - private Bundle downloadCACert() { - Bundle result = new Bundle(); - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); - String cert_string = downloadWithCommercialCA(ca_cert_url); - result.putBoolean(RESULT_KEY, true); - - if (validCertificate(cert_string) && go_ahead) { - preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); - result.putBoolean(RESULT_KEY, true); - } else { - String reason_to_fail = pickErrorMessage(cert_string); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - } catch (JSONException e) { - String reason_to_fail = formatErrorMessage(malformed_url); - result.putString(ERRORS, reason_to_fail); - result.putBoolean(RESULT_KEY, false); - } - - return result; - } - - - public static boolean caCertDownloaded() { - return CA_CERT_DOWNLOADED; - } - - private boolean validCertificate(String cert_string) { - boolean result = false; - if (!ConfigHelper.checkErroneousDownload(cert_string)) { - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(cert_string); - try { - if (certificate != null) { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - 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)); - - result = real_fingerprint.trim().equalsIgnoreCase(expected_fingerprint.trim()); - } else - result = false; - } catch (JSONException e) { - result = false; - } catch (NoSuchAlgorithmException e) { - result = false; - } catch (CertificateEncodingException e) { - result = false; - } - } - - 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)); - } - return hexData.toString(); - } - private Bundle getAndSetProviderJson(String provider_main_url, String provider_ca_cert_fingerprint) { Bundle result = new Bundle(); @@ -807,7 +128,12 @@ public class ProviderAPI extends IntentService { return result; } - private Bundle getAndSetEipServiceJson() { + /** + * Downloads the eip-service.json from a given URL, and saves eip service capabilities including the offered gateways + * @return a bundle with a boolean value mapped to a key named RESULT_KEY, and which is true if the download was successful. + */ + @Override + protected Bundle getAndSetEipServiceJson() { Bundle result = new Bundle(); String eip_service_json_string = ""; if (go_ahead) { @@ -831,23 +157,58 @@ public class ProviderAPI extends IntentService { } /** - * Interprets the error message as a JSON object and extract the "errors" keyword pair. - * If the error message is not a JSON object, then it is returned untouched. + * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. * - * @param string_json_error_message - * @return final error message + * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. */ - private String pickErrorMessage(String string_json_error_message) { - String error_message = ""; + @Override + protected boolean updateVpnCertificate() { try { - JSONObject json_error_message = new JSONObject(string_json_error_message); - error_message = json_error_message.getString(ERRORS); + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + + String provider_main_url = provider_json.getString(Provider.API_URL); + URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); + + String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); + + if (cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) + return false; + else + return loadCertificate(cert_string); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return false; } catch (JSONException e) { // TODO Auto-generated catch block - error_message = string_json_error_message; + e.printStackTrace(); + return false; + } + } + + private Bundle downloadCACert() { + Bundle result = new Bundle(); + try { + JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); + String ca_cert_url = provider_json.getString(Provider.CA_CERT_URI); + String cert_string = downloadWithCommercialCA(ca_cert_url); + result.putBoolean(RESULT_KEY, true); + + if (validCertificate(cert_string) && go_ahead) { + preferences.edit().putString(Provider.CA_CERT, cert_string).commit(); + result.putBoolean(RESULT_KEY, true); + } else { + String reason_to_fail = pickErrorMessage(cert_string); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); + } + } catch (JSONException e) { + String reason_to_fail = formatErrorMessage(malformed_url); + result.putString(ERRORS, reason_to_fail); + result.putBoolean(RESULT_KEY, false); } - return error_message; + return result; } //TODO: refactor with ticket #8773 @@ -861,7 +222,7 @@ public class ProviderAPI extends IntentService { HttpsURLConnection connection = PinningHelper.getPinnedHttpsURLConnection(Dashboard.getContext(), pins, url); connection.setConnectTimeout(seconds_of_timeout * 1000); if (!LeapSRPSession.getToken().isEmpty()) - connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token = " + LeapSRPSession.getToken()); + connection.addRequestProperty(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); result = new Scanner(connection.getInputStream()).useDelimiter("\\A").next(); } catch (IOException e) { if(e instanceof SSLHandshakeException) @@ -879,8 +240,7 @@ public class ProviderAPI extends IntentService { * @param string_url * @return */ - private String downloadWithCommercialCA(String string_url) { - + protected String downloadWithCommercialCA(String string_url) { String responseString; JSONObject errorJson = new JSONObject(); @@ -908,24 +268,13 @@ public class ProviderAPI extends IntentService { return responseString; } - - @NonNull - private List> getAuthorizationHeader() { - List> headerArgs = new ArrayList<>(); - if (!LeapSRPSession.getToken().isEmpty()) { - Pair authorizationHeaderPair = new Pair<>(LeapSRPSession.AUTHORIZATION_HEADER, "Token token=" + LeapSRPSession.getToken()); - headerArgs.add(authorizationHeaderPair); - } - return headerArgs; - } - /** * Tries to download the contents of the provided url using not commercially validated CA certificate from chosen provider. * * @param urlString as a string * @return an empty string if it fails, the url content if not. */ - private String downloadWithProviderCA(String urlString) { + protected String downloadWithProviderCA(String urlString) { JSONObject initError = new JSONObject(); String responseString; @@ -941,89 +290,4 @@ public class ProviderAPI extends IntentService { return responseString; } - private boolean logOut() { - OkHttpClient okHttpClient = initSelfSignedCAHttpClient(new JSONObject()); - if (okHttpClient == null) { - return false; - } - - String deleteUrl = provider_api_url + "/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; - } - return true; - } - - /** - * Downloads a new OpenVPN certificate, attaching authenticated cookie for authenticated certificate. - * - * @return true if certificate was downloaded correctly, false if provider.json is not present in SharedPreferences, or if the certificate url could not be parsed as a URI, or if there was an SSL error. - */ - private boolean updateVpnCertificate() { - try { - JSONObject provider_json = new JSONObject(preferences.getString(Provider.KEY, "")); - - String provider_main_url = provider_json.getString(Provider.API_URL); - URL new_cert_string_url = new URL(provider_main_url + "/" + provider_json.getString(Provider.API_VERSION) + "/" + Constants.VPN_CERTIFICATE); - - String cert_string = downloadWithProviderCA(new_cert_string_url.toString()); - - if (cert_string.isEmpty() || ConfigHelper.checkErroneousDownload(cert_string)) - return false; - else - return loadCertificate(cert_string); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } catch (JSONException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } - - //FIXME: don't save private keys in shared preferences! use the keystore - private boolean loadCertificate(String cert_string) { - try { - // API returns concatenated cert & key. Split them for OpenVPN options - String certificateString = null, keyString = null; - String[] certAndKey = cert_string.split("(?<=-\n)"); - for (int i = 0; i < certAndKey.length - 1; i++) { - if (certAndKey[i].contains("KEY")) { - keyString = certAndKey[i++] + certAndKey[i]; - } else if (certAndKey[i].contains("CERTIFICATE")) { - certificateString = certAndKey[i++] + certAndKey[i]; - } - } - - RSAPrivateKey key = ConfigHelper.parseRsaKeyFromString(keyString); - keyString = Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.PRIVATE_KEY, "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "-----END RSA PRIVATE KEY-----").commit(); - - X509Certificate certificate = ConfigHelper.parseX509CertificateFromString(certificateString); - certificateString = Base64.encodeToString(certificate.getEncoded(), Base64.DEFAULT); - preferences.edit().putString(Constants.VPN_CERTIFICATE, "-----BEGIN CERTIFICATE-----\n" + certificateString + "-----END CERTIFICATE-----").commit(); - return true; - } catch (CertificateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - return false; - } - } } -- cgit v1.2.3 From 5df197df4b82251467465815f503a2b38a36166b Mon Sep 17 00:00:00 2001 From: cyBerta Date: Tue, 31 Oct 2017 16:39:07 +0100 Subject: remove legacy code from SessionDialog --- .../main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java index 88dec39b..d124c395 100644 --- a/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java +++ b/app/src/main/java/se/leap/bitmaskclient/userstatus/SessionDialog.java @@ -50,8 +50,7 @@ public class SessionDialog extends DialogFragment { public static enum ERRORS { USERNAME_MISSING, PASSWORD_INVALID_LENGTH, - RISEUP_WARNING, - INITIALIZATION_ERROR + RISEUP_WARNING } @InjectView(R.id.user_message) @@ -123,9 +122,6 @@ public class SessionDialog extends DialogFragment { else if (arguments.containsKey(ERRORS.RISEUP_WARNING.toString())) { user_message.setVisibility(VISIBLE); user_message.setText(R.string.login_riseup_warning); - } else if (arguments.containsKey(ERRORS.INITIALIZATION_ERROR.toString())) { - user_message.setVisibility(VISIBLE); - user_message.setText(String.valueOf(arguments.get(ERRORS.INITIALIZATION_ERROR.toString()))); } if (arguments.containsKey(USERNAME)) { String username = arguments.getString(USERNAME); -- cgit v1.2.3