diff --git a/app/build.gradle b/app/build.gradle index b05e88672..86512389a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -38,6 +38,7 @@ dependencies { compile 'com.mikhaellopez:circularfillableloaders:1.2.0' compile 'com.squareup.retrofit2:retrofit:2.2.0' compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.okhttp3:logging-interceptor:3.6.0' compile 'com.github.chrisbanes:PhotoView:1.3.1' compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.github.arimorty:floatingsearchview:2.0.3' diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index e34744c47..a83bb9e05 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -34,7 +34,10 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import okhttp3.ConnectionSpec; import okhttp3.Dispatcher; import okhttp3.Interceptor; import okhttp3.OkHttpClient; @@ -114,32 +117,13 @@ public class BaseActivity extends AppCompatActivity { protected void createMastodonAPI() { mastodonApiDispatcher = new Dispatcher(); - OkHttpClient okHttpClient = new OkHttpClient.Builder() - .addInterceptor(new Interceptor() { - @Override - public Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - - Request.Builder builder = originalRequest.newBuilder(); - String accessToken = getAccessToken(); - if (accessToken != null) { - builder.header("Authorization", String.format("Bearer %s", accessToken)); - } - Request newRequest = builder.build(); - - return chain.proceed(newRequest); - } - }) - .dispatcher(mastodonApiDispatcher) - .build(); - Gson gson = new GsonBuilder() .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) .create(); Retrofit retrofit = new Retrofit.Builder() .baseUrl(getBaseUrl()) - .client(okHttpClient) + .client(OkHttpUtils.getCompatibleClient()) .addConverterFactory(GsonConverterFactory.create(gson)) .build(); @@ -149,6 +133,7 @@ public class BaseActivity extends AppCompatActivity { protected void createTuskyAPI() { Retrofit retrofit = new Retrofit.Builder() .baseUrl(getString(R.string.tusky_api_url)) + .client(OkHttpUtils.getCompatibleClient()) .build(); tuskyAPI = retrofit.create(TuskyAPI.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java index 11ede95b7..29712d0f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java @@ -117,6 +117,7 @@ public class LoginActivity extends AppCompatActivity { private MastodonAPI getApiFor(String domain) { Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://" + domain) + .client(OkHttpUtils.getCompatibleClient()) .addConverterFactory(GsonConverterFactory.create()) .build(); @@ -168,8 +169,10 @@ public class LoginActivity extends AppCompatActivity { }; try { - getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES, - getString(R.string.app_website)).enqueue(callback); + getApiFor(domain) + .authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), + OAUTH_SCOPES, getString(R.string.app_website)) + .enqueue(callback); } catch (IllegalArgumentException e) { editText.setError(getString(R.string.error_invalid_domain)); } @@ -234,7 +237,7 @@ public class LoginActivity extends AppCompatActivity { } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "The app version was not found. " + e.getMessage()); } - if (preferences.getInt("lastUpdate", 0) != versionCode) { + if (preferences.getInt("lastUpdateVersion", 0) != versionCode) { SharedPreferences.Editor editor = preferences.edit(); if (versionCode == 14) { /* This version switches the order of scheme and host in the OAuth redirect URI. @@ -243,7 +246,7 @@ public class LoginActivity extends AppCompatActivity { * "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */ editor.clear(); } - editor.putInt("lastUpdate", versionCode); + editor.putInt("lastUpdateVersion", versionCode); editor.apply(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java index ec3300618..4e963d684 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java +++ b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseInstanceIdService.java @@ -20,6 +20,7 @@ public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService { protected void createTuskyAPI() { Retrofit retrofit = new Retrofit.Builder() .baseUrl(getString(R.string.tusky_api_url)) + .client(OkHttpUtils.getCompatibleClient()) .build(); tuskyAPI = retrofit.create(TuskyAPI.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java index 0031080ce..2ef29e37d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java +++ b/app/src/main/java/com/keylesspalace/tusky/MyFirebaseMessagingService.java @@ -84,7 +84,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService { final String domain = preferences.getString("domain", null); final String accessToken = preferences.getString("accessToken", null); - OkHttpClient okHttpClient = new OkHttpClient.Builder() + OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder() .addInterceptor(new Interceptor() { @Override public okhttp3.Response intercept(Chain chain) throws IOException { diff --git a/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java new file mode 100644 index 000000000..bb25b7161 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/OkHttpUtils.java @@ -0,0 +1,157 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU + * Lesser General Public License as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * Tusky 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 Lesser + * General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License along with Tusky. If + * not, see . */ + +package com.keylesspalace.tusky; + +import android.os.Build; +import android.support.annotation.NonNull; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.ConnectionSpec; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; + +class OkHttpUtils { + private static final String TAG = "OkHttpUtils"; // logging tag + + @NonNull + static OkHttpClient.Builder getCompatibleClientBuilder() { + ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledCipherSuites() + .supportsTlsExtensions(true) + .build(); + + List specList = new ArrayList<>(); + specList.add(ConnectionSpec.MODERN_TLS); + specList.add(fallback); + specList.add(ConnectionSpec.CLEARTEXT); + + HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); + loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectionSpecs(specList) + .addInterceptor(loggingInterceptor); + + return enableHigherTlsOnPreLollipop(builder); + } + + @NonNull + static OkHttpClient getCompatibleClient() { + return getCompatibleClientBuilder().build(); + } + + private static OkHttpClient.Builder enableHigherTlsOnPreLollipop(OkHttpClient.Builder builder) { + if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + + X509TrustManager trustManager = (X509TrustManager) trustManagers[0]; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, new TrustManager[] { trustManager }, null); + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + + builder.sslSocketFactory(new Tls11And12SocketFactory(sslSocketFactory), + trustManager); + } catch (NoSuchAlgorithmException|KeyStoreException|KeyManagementException e) { + Log.e(TAG, "Failed enabling TLS 1.1 & 1.2. " + e.getMessage()); + } + } + + return builder; + } + + private static class Tls11And12SocketFactory extends SSLSocketFactory { + private static final String[] TLS_VERSIONS = { "TLSv1.1", "TLSv1.2" }; + + final SSLSocketFactory delegate; + + Tls11And12SocketFactory(SSLSocketFactory base) { + this.delegate = base; + } + + @Override + public String[] getDefaultCipherSuites() { + return delegate.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return delegate.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return patch(delegate.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException, UnknownHostException { + return patch(delegate.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return patch(delegate.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + return patch(delegate.createSocket(address, port, localAddress, localPort)); + } + + private Socket patch(Socket socket) { + if (socket instanceof SSLSocket) { + SSLSocket sslSocket = (SSLSocket) socket; + sslSocket.setEnabledProtocols(TLS_VERSIONS); + } + return socket; + } + } +}