I have an Apache/Coyote server running on Google Cloud with TLS 1.2 only, and one of our customers devices runs JellyBean 4.1.2 API 16. It is important we get this device working, but I am stuck trying to enable TLS 1.2 on the older Android version. It works on newer devices.
I have tried a few different ways. 1 - Standard Android HTTPS, with BouncyCastle security provider 2 - Custom SSLSocketFactory 3 - BouncyCastle HTTPS request using TLSProtocol
The closest result was number 3, but the documentation is sparse so I couldn't complete it. I'm told that I should be able to get it working with option 1, but haven't been able to figure it out.
I'll paste my code below and if someone can correct me, that would be appreciated.
This is the standard android way, but also notice I'm setting the socketfactory, eg urlConnection.setSSLSocketFactory(new TLSSocketFactory());
private void useAndroidHTTP() {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
HttpsURLConnection conn = getConnection(FULL_URL);
InputStream is = conn.getInputStream();
String result = readStream(is);
Log.d(TAG, "Result: " + result);
}
private HttpsURLConnection getConnection(String url) throws MalformedURLException {
URL request_url = new URL(url);
listProviders();
urlConnection = (HttpsURLConnection) request_url.openConnection();
urlConnection.setSSLSocketFactory(new TLSSocketFactory());
listProviders();
urlConnection.setRequestMethod("GET");
urlConnection.setReadTimeout(15 * 1000);
urlConnection.setConnectTimeout(15 * 1000);
urlConnection.connect()
return urlConnection;
}
I've also tried multiple sample SocketFactory's that I have found online. For example:
public class TLSSocketFactory extends javax.net.ssl.SSLSocketFactory {
private javax.net.ssl.SSLSocketFactory delegate;
static {
Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1);
}
public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(null, null, null);
delegate = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return new String[] {
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
};
// return delegate.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSOnSocket(delegate.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(delegate.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return enableTLSOnSocket(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
return enableTLSOnSocket(delegate.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(delegate.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(delegate.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSOnSocket(Socket socket) {
if (socket != null && (socket instanceof SSLSocket)) {
((SSLSocket) socket).setEnabledProtocols(new String[]{"TLSv1.2"});
}
return socket;
}
}
Another:
public class SimpleSSLSocketFactory extends javax.net.ssl.SSLSocketFactory {
private javax.net.ssl.SSLSocketFactory delegate;
public SimpleSSLSocketFactory() {
try {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, // KeyManager not required
new TrustManager[]{new DummyTrustManager()},
new java.security.SecureRandom());
delegate = sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException e) {
System.out.println(e);
} catch (KeyManagementException e) {
System.out.println(e);
}
}
public static SocketFactory getDefault() {
return new SimpleSSLSocketFactory();
}
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return delegate.createSocket(socket, host, port, autoClose);
}
public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr2, int j) throws IOException {
return delegate.createSocket(inaddr, i, inaddr2, j);
}
public Socket createSocket(InetAddress inaddr, int i) throws IOException {
return delegate.createSocket(inaddr, i);
}
public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
return delegate.createSocket(s, i, inaddr, j);
}
public Socket createSocket(String s, int i) throws IOException {
return delegate.createSocket(s, i);
}
public String[] getDefaultCipherSuites() {
return delegate.getSupportedCipherSuites();
}
public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
private static class DummyTrustManager implements X509TrustManager {
public boolean isClientTrusted(X509Certificate[] cert) {
return true;
}
public boolean isServerTrusted(X509Certificate[] cert) {
try {
cert[0].checkValidity();
return true;
} catch (CertificateExpiredException e) {
return false;
} catch (CertificateNotYetValidException e) {
return false;
}
}
public void checkClientTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws CertificateException {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] x509Certificates, String s) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
}
I also heard the PayPal app uses TLS1.2 for older android devices so that gave me hope, but of course it doesn't work. (I suspect they support older TLS versions alongside 1.2)
public class PaypalSocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
public PaypalSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, null, null);
internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@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 instanceof SSLSocket) {
ArrayList<String> supportedProtocols =
new ArrayList<>(Arrays.asList(((SSLSocket) socket).getSupportedProtocols()));
supportedProtocols.retainAll(Arrays.asList("TLSv1.2"));
((SSLSocket) socket).setEnabledProtocols(supportedProtocols.toArray(
new String[supportedProtocols.size()]));
}
return socket;
}
}
I then tried SpongyCastle
, the Android version of BouncyCastle
.
private static void useSpongyCastle() throws IOException {
Security.insertProviderAt(new BouncyCastleProvider(), 1);
java.security.SecureRandom secureRandom = new java.security.SecureRandom();
// Socket socket = new Socket(java.net.InetAddress.getByName(DOMAIN), 443);
Socket socket = new Socket(DOMAIN, 443);
TlsClientProtocol protocol = new TlsClientProtocol(socket.getInputStream(), socket.getOutputStream(), secureRandom);
DefaultTlsClient client = new DefaultTlsClient() {
public TlsAuthentication getAuthentication() throws IOException {
TlsAuthentication auth = new TlsAuthentication() {
// Capture the server certificate information!
public void notifyServerCertificate(Certificate serverCertificate) throws IOException {
}
public TlsCredentials getClientCredentials(CertificateRequest certificateRequest) throws IOException {
return null;
}
};
return auth;
}
};
protocol.connect(client);
java.io.OutputStream output = protocol.getOutputStream();
output.write("GET / HTTP/1.1\r\n".getBytes("UTF-8"));
output.write(("Host: " + FULL_URL + "\r\n").getBytes("UTF-8"));
//Get auth
Log.d(TAG, "AUTH: " + BasicAuthentication.getAuthenticationHeader("myuser", "mypass"));
output.write(("Authorization: " + BasicAuthentication.getAuthenticationHeader("myuser", "mypass")).getBytes());
// output.write("Content-Type: application/json".getBytes());
// output.write("Accept: application/json".getBytes());
output.write("Connection: close\r\n".getBytes("UTF-8")); // So the server will close socket immediately.
output.write("\r\n".getBytes("UTF-8")); // HTTP1.1 requirement: last line must be empty line.
output.flush();
java.io.InputStream input = protocol.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String line;
StringBuilder sb = new StringBuilder();
try {
while ((line = reader.readLine()) != null) {
Log.d(TAG, "--> " + line);
sb.append(line).append("\n");
}
} catch (TlsNoCloseNotifyException e) {
Log.d(TAG, "End of stream");
}
String result = sb.toString();
Log.d(TAG, "Result: " + result);
listProviders();
}
All of the above methods, except SpongyCastle, give me an SSLHandshakeException. SpongyCastle gives me a HTTP 400. I think there's some issue with the "HOST" header, as my server is on a sub-domain and doesn't start with www
. This is a guess though so I'm not sure.
So now I'm at the stage where I'm either really close, or I've gone down completely the wrong path.
Can anyone give me a sample app that works with TLS v1.2, or point out what I've done wrong in the above examples?
The solution here was to use the standard Android HTTP libs and add this method to my custom SSLSocketFactory:
private static void setupSecurityForTLS() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
Log.d(TAG, "Adding new security provider");
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Security.insertProviderAt(new BouncyCastleProvider(), 2);
}
}
and call it BEFORE I initialise the socket.
You can also add the 2 lines to a static initialiser at the top of the class, eg:
static {
Security.insertProviderAt(new BouncyCastleJsseProvider(), 1);
Security.insertProviderAt(new BouncyCastleProvider(), 2);
}