Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 67b0edd

Browse files
authored
Merge pull request #576 from cloudant/force-tls1.2-on-old-android
Force tls1.2 on old android
2 parents b58256c + 4e3b35e commit 67b0edd

File tree

6 files changed

+193
-77
lines changed

6 files changed

+193
-77
lines changed

AndroidTest/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ task(clearDeviceLog, type: AndroidExec) {
124124
task(pullDeviceLog, type: AndroidExec) {
125125
doFirst {
126126
def logPrefix = System.getenv('TEST_ENV_NAME')
127-
if (logPrefix == null) logPrefix = UUID.randomUUID()
127+
if (logPrefix == null) logPrefix = UUID.randomUUID().toString()
128128
standardOutput = new FileOutputStream(new File(reportsDir, logPrefix + "_logcat.log"), false)
129129
}
130130
commandLine "adb","logcat", "-d", "-v", "threadtime"

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Unreleased
2+
- [IMPROVED] Forced a TLS1.2 `SSLSocketFactory` where possible on Android API versions < 20 (it is
3+
already enabled by default on newer API levels).
4+
15
# 2.2.0 (2018-02-14)
26
- [NEW] Added API for specifying a mango selector in the filtered pull replicator
37
- [IMPROVED] Improved efficiency of sub-query when picking winning

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/internal/mazha/CouchClient.java

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.cloudant.http.HttpConnection;
2626
import com.cloudant.http.HttpConnectionRequestInterceptor;
2727
import com.cloudant.http.HttpConnectionResponseInterceptor;
28+
import com.cloudant.http.internal.interceptors.SSLCustomizerInterceptor;
29+
import com.cloudant.http.internal.interceptors.UserAgentInterceptor;
2830
import com.cloudant.sync.internal.common.RetriableTask;
2931
import com.cloudant.sync.internal.documentstore.DocumentRevsList;
3032
import com.cloudant.sync.internal.documentstore.MultipartAttachmentWriter;
@@ -39,11 +41,16 @@
3941
import java.io.InputStream;
4042
import java.io.InputStreamReader;
4143
import java.lang.reflect.Type;
44+
import java.net.InetAddress;
45+
import java.net.Socket;
4246
import java.net.URI;
4347
import java.nio.charset.Charset;
48+
import java.security.KeyManagementException;
49+
import java.security.NoSuchAlgorithmException;
4450
import java.util.ArrayList;
4551
import java.util.Arrays;
4652
import java.util.Collection;
53+
import java.util.Collections;
4754
import java.util.HashMap;
4855
import java.util.List;
4956
import java.util.Map;
@@ -52,8 +59,25 @@
5259
import java.util.logging.Level;
5360
import java.util.logging.Logger;
5461

62+
import javax.net.ssl.SSLContext;
63+
import javax.net.ssl.SSLSocket;
64+
import javax.net.ssl.SSLSocketFactory;
65+
5566
public class CouchClient {
5667

68+
public static final List<HttpConnectionRequestInterceptor> DEFAULT_REQUEST_INTERCEPTORS;
69+
70+
// Set up the defaults
71+
static {
72+
HttpConnectionRequestInterceptor ua_interceptor =
73+
new UserAgentInterceptor(CouchClient.class.getClassLoader(),
74+
"META-INF/com.cloudant.sync.client.properties");
75+
HttpConnectionRequestInterceptor tlsInterceptor = checkAndGetTlsInterceptor();
76+
DEFAULT_REQUEST_INTERCEPTORS = (tlsInterceptor != null) ?
77+
Arrays.asList(ua_interceptor, tlsInterceptor) :
78+
Collections.singletonList(ua_interceptor);
79+
}
80+
5781
private CouchURIHelper uriHelper;
5882
private List<HttpConnectionRequestInterceptor> requestInterceptors;
5983
private List<HttpConnectionResponseInterceptor> responseInterceptors;
@@ -66,6 +90,8 @@ public CouchClient(URI rootUri,
6690
this.requestInterceptors = new ArrayList<HttpConnectionRequestInterceptor>();
6791
this.responseInterceptors = new ArrayList<HttpConnectionResponseInterceptor>();
6892

93+
this.requestInterceptors.addAll(DEFAULT_REQUEST_INTERCEPTORS);
94+
6995
if (requestInterceptors != null) {
7096
this.requestInterceptors.addAll(requestInterceptors);
7197
}
@@ -75,6 +101,121 @@ public CouchClient(URI rootUri,
75101
}
76102
}
77103

104+
private static SSLCustomizerInterceptor checkAndGetTlsInterceptor() {
105+
// Some assistance for TLSv1.2 support. Two things we check before we try to force TLSv1.2
106+
// so that we don't interfere with any other custom configuration that may have been
107+
// provided:
108+
// i) are we running on Android, and an old version of Android (api level < 20 does not
109+
// have TLSv1.2 enabled by default)
110+
// ii) does the default SSLContext have TLSv1.2 enabled or available
111+
if (Misc.isRunningOnAndroid()) {
112+
// This block catches all exceptions from TLSv1.2 checks, if we get exceptions we bail
113+
// with a RuntimeException
114+
try {
115+
// Get the API level reflectively so we don't need to import classes only available
116+
// in Android
117+
int androidApiLevel = Class.forName("android.os.Build$VERSION").getField
118+
("SDK_INT").getInt(null);
119+
// If we are on an old version of Android and TLSv1.2 has not been enabled already
120+
// on the default context then we add a special interceptor
121+
if (androidApiLevel < 20) {
122+
// Check i has passed we are on an old version of Android
123+
124+
// Get the default SSLContext
125+
SSLContext defSslCtx = SSLContext.getDefault();
126+
127+
// Check the default protocols for TLSv1.2
128+
if (!Arrays.asList(defSslCtx.getDefaultSSLParameters().getProtocols())
129+
.contains(TlsOnlySslSocketFactory.TLSv12) &&
130+
Arrays.asList(defSslCtx.getSupportedSSLParameters().getProtocols())
131+
.contains(TlsOnlySslSocketFactory.TLSv12)) {
132+
// Check ii has passed TLSv1.2 is not already enabled on the default context
133+
// but is supported on the default context so we can use it
134+
135+
// In an ideal world we would also check the default SSLSocketFactory that
136+
// is set on the HttpsUrlConnection to see if that has been customized from
137+
// the default, but there isn't really a way to check this, equals is not
138+
// overridden on the SSLSocketFactory and there is no route back to the
139+
// SSLContext that generated the SSLSocketFactory.
140+
SSLContext sc = SSLContext.getInstance(TlsOnlySslSocketFactory.TLSv12);
141+
sc.init(null, null, null);
142+
// We add this interceptor, but it could be overridden by a later user
143+
// provided interceptor, so this shouldn't change anyone's existing
144+
// customizations.
145+
SSLSocketFactory s = sc.getSocketFactory();
146+
return new SSLCustomizerInterceptor(new TlsOnlySslSocketFactory(s));
147+
}
148+
}
149+
} catch (NoSuchAlgorithmException e) {
150+
throw new RuntimeException(e);
151+
} catch (NoSuchFieldException e) {
152+
throw new RuntimeException(e);
153+
} catch (IllegalAccessException e) {
154+
throw new RuntimeException(e);
155+
} catch (ClassNotFoundException e) {
156+
throw new RuntimeException(e);
157+
} catch (KeyManagementException e) {
158+
throw new RuntimeException(e);
159+
}
160+
}
161+
return null;
162+
}
163+
164+
private static final class TlsOnlySslSocketFactory extends SSLSocketFactory {
165+
166+
private static final String TLSv12 = "TLSv1.2";
167+
private static final String[] TLSv12Only = new String[]{TLSv12};
168+
private final SSLSocketFactory delegate;
169+
170+
private TlsOnlySslSocketFactory(SSLSocketFactory delegate) {
171+
this.delegate = delegate;
172+
}
173+
174+
@Override
175+
public String[] getDefaultCipherSuites() {
176+
return delegate.getDefaultCipherSuites();
177+
}
178+
179+
@Override
180+
public String[] getSupportedCipherSuites() {
181+
return delegate.getSupportedCipherSuites();
182+
}
183+
184+
@Override
185+
public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException {
186+
return tlsOnlySocket(delegate.createSocket(socket, s, i, b));
187+
}
188+
189+
@Override
190+
public Socket createSocket(String s, int i) throws IOException {
191+
return tlsOnlySocket(delegate.createSocket(s, i));
192+
}
193+
194+
@Override
195+
public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws
196+
IOException {
197+
return tlsOnlySocket(delegate.createSocket(s, i, inetAddress, i1));
198+
}
199+
200+
@Override
201+
public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
202+
return tlsOnlySocket(delegate.createSocket(inetAddress, i));
203+
}
204+
205+
@Override
206+
public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int
207+
i1) throws IOException {
208+
return tlsOnlySocket(delegate.createSocket(inetAddress, i, inetAddress1, i1));
209+
}
210+
211+
private Socket tlsOnlySocket(Socket s) {
212+
if (s instanceof SSLSocket) {
213+
((SSLSocket) s).setEnabledProtocols(TLSv12Only);
214+
}
215+
return s;
216+
}
217+
}
218+
78219
public URI getRootUri() {
79220
return this.uriHelper.getRootUri();
80221
}

cloudant-sync-datastore-core/src/main/java/com/cloudant/sync/replication/ReplicatorBuilder.java

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.cloudant.http.HttpConnectionResponseInterceptor;
1919
import com.cloudant.http.internal.interceptors.CookieInterceptor;
2020
import com.cloudant.http.internal.interceptors.IamCookieInterceptor;
21-
import com.cloudant.http.internal.interceptors.UserAgentInterceptor;
2221
import com.cloudant.sync.documentstore.DocumentStore;
2322
import com.cloudant.sync.internal.replication.PullStrategy;
2423
import com.cloudant.sync.internal.replication.PushStrategy;
@@ -40,10 +39,6 @@
4039
// S = Source Type, T = target Type, E = Extending class Type
4140
public abstract class ReplicatorBuilder<S, T, E> {
4241

43-
private static final UserAgentInterceptor USER_AGENT_INTERCEPTOR =
44-
new UserAgentInterceptor(ReplicatorBuilder.class.getClassLoader(),
45-
"META-INF/com.cloudant.sync.client.properties");
46-
4742
private T target;
4843

4944
private S source;
@@ -62,10 +57,6 @@ public abstract class ReplicatorBuilder<S, T, E> {
6257

6358
private String iamApiKey = null;
6459

65-
private ReplicatorBuilder() {
66-
requestInterceptors.add(USER_AGENT_INTERCEPTOR);
67-
}
68-
6960
private int getDefaultPort(URI uri) {
7061

7162
String uriProtocol = uri.getScheme();

cloudant-sync-datastore-core/src/test/java/com/cloudant/http/HttpTest.java

Lines changed: 35 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.cloudant.sync.internal.util.JSONUtils;
2424

2525
import org.junit.Assert;
26+
import org.junit.Before;
2627
import org.junit.Test;
2728
import org.junit.experimental.categories.Category;
2829

@@ -40,6 +41,32 @@
4041
public class HttpTest extends CouchTestBase {
4142

4243
private String data = "{\"hello\":\"world\"}";
44+
private ByteArrayInputStream bis;
45+
46+
@Before
47+
public void setupDataByteStream() {
48+
this.bis = new ByteArrayInputStream(data.getBytes());
49+
}
50+
51+
private HttpConnection postAndAssertNothingReadBeforeSettingBodyGenerator(CouchConfig config)
52+
throws Exception {
53+
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
54+
"application/json");
55+
56+
// nothing read from stream
57+
Assert.assertEquals(bis.available(), data.getBytes().length);
58+
59+
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
60+
@Override
61+
public InputStream getInputStream() {
62+
return bis;
63+
}
64+
});
65+
// This test invokes HttpConnection directly rather than via the CouchClient, so we need to
66+
// force the addition of some default interceptors
67+
conn.requestInterceptors.addAll(CouchClient.DEFAULT_REQUEST_INTERCEPTORS);
68+
return conn;
69+
}
4370

4471
/*
4572
* Test "Expect: 100-Continue" header works as expected
@@ -54,21 +81,9 @@ public class HttpTest extends CouchTestBase {
5481
* whilst we are still writing).
5582
*/
5683
@Test
57-
public void testExpect100Continue() throws IOException {
84+
public void testExpect100Continue() throws Exception {
5885
CouchConfig config = getCouchConfig("no_such_database");
59-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
60-
"application/json");
61-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
62-
63-
// nothing read from stream
64-
Assert.assertEquals(bis.available(), data.getBytes().length);
65-
66-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
67-
@Override
68-
public InputStream getInputStream() {
69-
return bis;
70-
}
71-
});
86+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
7287
boolean thrown = false;
7388
try {
7489
conn.execute();
@@ -87,24 +102,12 @@ public InputStream getInputStream() {
87102
* Basic test that we can write a document body by POSTing to a known database
88103
*/
89104
@Test
90-
public void testWriteToServerOk() throws IOException {
105+
public void testWriteToServerOk() throws Exception {
91106
CouchConfig config = getCouchConfig("httptest" + System.currentTimeMillis());
92107
CouchClient client = new CouchClient(config.getRootUri(), config.getRequestInterceptors()
93108
, config.getResponseInterceptors());
94109
client.createDb();
95-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
96-
"application/json");
97-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
98-
99-
// nothing read from stream
100-
Assert.assertEquals(bis.available(), data.getBytes().length);
101-
102-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
103-
@Override
104-
public InputStream getInputStream() {
105-
return bis;
106-
}
107-
});
110+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
108111
conn.execute();
109112

110113
// stream was read to end
@@ -117,24 +120,12 @@ public InputStream getInputStream() {
117120
* without first calling execute()
118121
*/
119122
@Test
120-
public void testReadBeforeExecute() throws IOException {
123+
public void testReadBeforeExecute() throws Exception {
121124
CouchConfig config = getCouchConfig("httptest" + System.currentTimeMillis());
122125
CouchClient client = new CouchClient(config.getRootUri(), config.getRequestInterceptors()
123126
, config.getResponseInterceptors());
124127
client.createDb();
125-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
126-
"application/json");
127-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
128-
129-
// nothing read from stream
130-
Assert.assertEquals(bis.available(), data.getBytes().length);
131-
132-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
133-
@Override
134-
public InputStream getInputStream() {
135-
return bis;
136-
}
137-
});
128+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
138129
try {
139130
conn.responseAsString();
140131
Assert.fail("IOException not thrown as expected");
@@ -156,7 +147,7 @@ public InputStream getInputStream() {
156147
// be named cookie_test
157148
//
158149
@Test
159-
public void testCookieAuthWithoutRetry() throws IOException {
150+
public void testCookieAuthWithoutRetry() throws Exception {
160151

161152
if (TestOptions.IGNORE_AUTH_HEADERS) {
162153
return;
@@ -168,21 +159,9 @@ public void testCookieAuthWithoutRetry() throws IOException {
168159
.currentTimeMillis()).getRootUri().toString());
169160

170161
CouchConfig config = getCouchConfig("cookie_test");
171-
HttpConnection conn = new HttpConnection("POST", config.getRootUri().toURL(),
172-
"application/json");
162+
HttpConnection conn = postAndAssertNothingReadBeforeSettingBodyGenerator(config);
173163
conn.responseInterceptors.add(interceptor);
174164
conn.requestInterceptors.add(interceptor);
175-
final ByteArrayInputStream bis = new ByteArrayInputStream(data.getBytes());
176-
177-
// nothing read from stream
178-
Assert.assertEquals(bis.available(), data.getBytes().length);
179-
180-
conn.setRequestBody(new HttpConnection.InputStreamGenerator() {
181-
@Override
182-
public InputStream getInputStream() {
183-
return bis;
184-
}
185-
});
186165
conn.execute();
187166

188167
// stream was read to end

0 commit comments

Comments
 (0)