Possible fix to enable connections using TLS 1.1 and 1.2 on pre-Lollipop android versions. Also expands to the enabled list of cipher suites.

This commit is contained in:
Vavassor 2017-04-09 18:36:55 -04:00
parent bfc89b26bd
commit fd472fbe1f
6 changed files with 172 additions and 25 deletions

View file

@ -38,6 +38,7 @@ dependencies {
compile 'com.mikhaellopez:circularfillableloaders:1.2.0' compile 'com.mikhaellopez:circularfillableloaders:1.2.0'
compile 'com.squareup.retrofit2:retrofit:2.2.0' compile 'com.squareup.retrofit2:retrofit:2.2.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.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.github.chrisbanes:PhotoView:1.3.1'
compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar' compile 'com.mikepenz:google-material-typeface:3.0.1.0.original@aar'
compile 'com.github.arimorty:floatingsearchview:2.0.3' compile 'com.github.arimorty:floatingsearchview:2.0.3'

View file

@ -34,7 +34,10 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.ConnectionSpec;
import okhttp3.Dispatcher; import okhttp3.Dispatcher;
import okhttp3.Interceptor; import okhttp3.Interceptor;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
@ -114,32 +117,13 @@ public class BaseActivity extends AppCompatActivity {
protected void createMastodonAPI() { protected void createMastodonAPI() {
mastodonApiDispatcher = new Dispatcher(); 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() Gson gson = new GsonBuilder()
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter()) .registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
.create(); .create();
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getBaseUrl()) .baseUrl(getBaseUrl())
.client(okHttpClient) .client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create(gson)) .addConverterFactory(GsonConverterFactory.create(gson))
.build(); .build();
@ -149,6 +133,7 @@ public class BaseActivity extends AppCompatActivity {
protected void createTuskyAPI() { protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url)) .baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build(); .build();
tuskyAPI = retrofit.create(TuskyAPI.class); tuskyAPI = retrofit.create(TuskyAPI.class);

View file

@ -117,6 +117,7 @@ public class LoginActivity extends AppCompatActivity {
private MastodonAPI getApiFor(String domain) { private MastodonAPI getApiFor(String domain) {
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://" + domain) .baseUrl("https://" + domain)
.client(OkHttpUtils.getCompatibleClient())
.addConverterFactory(GsonConverterFactory.create()) .addConverterFactory(GsonConverterFactory.create())
.build(); .build();
@ -168,8 +169,10 @@ public class LoginActivity extends AppCompatActivity {
}; };
try { try {
getApiFor(domain).authenticateApp(getString(R.string.app_name), getOauthRedirectUri(), OAUTH_SCOPES, getApiFor(domain)
getString(R.string.app_website)).enqueue(callback); .authenticateApp(getString(R.string.app_name), getOauthRedirectUri(),
OAUTH_SCOPES, getString(R.string.app_website))
.enqueue(callback);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
editText.setError(getString(R.string.error_invalid_domain)); editText.setError(getString(R.string.error_invalid_domain));
} }
@ -234,7 +237,7 @@ public class LoginActivity extends AppCompatActivity {
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "The app version was not found. " + e.getMessage()); 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(); SharedPreferences.Editor editor = preferences.edit();
if (versionCode == 14) { if (versionCode == 14) {
/* This version switches the order of scheme and host in the OAuth redirect URI. /* 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". */ * "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
editor.clear(); editor.clear();
} }
editor.putInt("lastUpdate", versionCode); editor.putInt("lastUpdateVersion", versionCode);
editor.apply(); editor.apply();
} }
} }

View file

@ -20,6 +20,7 @@ public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
protected void createTuskyAPI() { protected void createTuskyAPI() {
Retrofit retrofit = new Retrofit.Builder() Retrofit retrofit = new Retrofit.Builder()
.baseUrl(getString(R.string.tusky_api_url)) .baseUrl(getString(R.string.tusky_api_url))
.client(OkHttpUtils.getCompatibleClient())
.build(); .build();
tuskyAPI = retrofit.create(TuskyAPI.class); tuskyAPI = retrofit.create(TuskyAPI.class);

View file

@ -84,7 +84,7 @@ public class MyFirebaseMessagingService extends FirebaseMessagingService {
final String domain = preferences.getString("domain", null); final String domain = preferences.getString("domain", null);
final String accessToken = preferences.getString("accessToken", null); final String accessToken = preferences.getString("accessToken", null);
OkHttpClient okHttpClient = new OkHttpClient.Builder() OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
.addInterceptor(new Interceptor() { .addInterceptor(new Interceptor() {
@Override @Override
public okhttp3.Response intercept(Chain chain) throws IOException { public okhttp3.Response intercept(Chain chain) throws IOException {

View file

@ -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 <http://www.gnu.org/licenses/>. */
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<ConnectionSpec> 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;
}
}
}