maandag 14 juli 2014

Android and SSL HTTPClient

When you're developing an Android application you will inevitably - in your lifetime - need to access a REST Service (or alike). Android provides basic HTTP communication framework out-of-the-box, but you'll need to go the extra mile when you want to use HTTP/S communications.

The additional "S" in "HTTPS" is added to "Securely" transfer data between the client and the server (or between two pairs). I'm not going to explain the concepts behind HTTPS, you can read that on wikipedia, but an important note is that HTTPS certificates signed by known CA's are generally not an issue: these are already available in Trusted Certificate Authorities on your Android distribution.

If you would have a self-signed certificate (for development purposes) or a certificate signed by a not-so-well-known CA, you might run into some trouble.

I've divided the blogpost in two sections: the first one is what definitely not to do. The second section is how you should do it.

For the sake of completion, I found my inspiration from this blog: http://blog.crazybob.org/2010/02/android-trusting-ssl-certificates.html.

Not the way to go

I'm a big fan of StackOverflow, unfortunately, it sometimes overflows of half-founded-solutions or even bad solutions. One of the bad solutions is the solution below. 

    private class MySSLSocketFactory extends SSLSocketFactory {
        SSLContext sslContext = SSLContext.getInstance("TLS");

        public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, 
                                                              KeyManagementException, KeyStoreException, 
                                                              UnrecoverableKeyException {
            super(truststore);

            TrustManager tm = new X509TrustManager() {
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                }

                public X509Certificate[] getAcceptedIssuers() {
                    return null;
                }
            };

            sslContext.init(null, new TrustManager[]{tm}, null);
        }

        @Override
        public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
            return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
        }

        @Override
        public Socket createSocket() throws IOException {
            return sslContext.getSocketFactory().createSocket();
        }
    }

In general, the above solution would allow ANY certificate to be accepted. Accepting any certificate could endanger data integrity, security, etc. You should always try to assess that the information is sent to the end-point you expect it to, and that the information is sent by the client you expect it from.


A better way to do it


The example below is using the BouncyCastle (BKS) algorithm.
import android.content.Context;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.security.KeyStore;


public class MyRestService {
    private final static String TAG = MyRestService.class.getSimpleName();

    private HttpClient httpClient = null;

    private Context context = null;

    public MyRestService(Context context) {
        this.context = context;

        this.httpClient = new MyHttpClient(this.context);
    }


    protected String post(URI api, Header[] headers, HttpEntity entity) /* ... throws  */ {
        String responseBody;

        try {
            HttpPost method = new HttpPost(api);
            method.setEntity(entity);
            method.setHeaders(headers);

            HttpResponse response = httpClient.execute(method);

            StatusLine status = response.getStatusLine();

            ByteArrayOutputStream out = new ByteArrayOutputStream();
            response.getEntity().writeTo(out);
            out.close();
            responseBody = out.toString();

            if (status.getStatusCode() != HttpStatus.SC_OK) {
                if (status.getStatusCode() == HttpStatus.SC_FORBIDDEN) {
                    //process forbidden status code
                } else if (status.getStatusCode() == HttpStatus.SC_REQUEST_TIMEOUT) {
                    //process request time out code
                } else {
                    //process unrecoverable errors
                }
                //...
            }
        } catch (IOException e) {
            //Log the exception, e.g. Log.e(TAG, e.getMessage(), e); or Logger.e(TAG, e);

            //process the exception
        }

        return responseBody;
    }

    protected Context getContext() {
        return context;
    }


    private class MyHttpClient extends DefaultHttpClient {

        final Context context;

        public MyHttpClient(Context ctx) {
            this.context = ctx;
        }

        @Override
        protected ClientConnectionManager createClientConnectionManager() {
            SchemeRegistry registry = new SchemeRegistry();

            registry.register("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register("https", newSslSocketFactory(), 443));

            HttpParams httpParameters = getParams();
            HttpConnectionParams.setConnectionTimeout(httpParameters, 10000);
            HttpConnectionParams.setSoTimeout(httpParameters, 10000);
            HttpProtocolParams.setVersion(httpParameters, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setUseExpectContinue(httpParameters, false);

            return new ThreadSafeClientConnManager(httpParameters, registry);
        }

        private SSLSocketFactory newSslSocketFactory() {
            try {
                KeyStore trusted = KeyStore.getInstance("BKS");
                InputStream in = this.context.getResources().openRawResource(R.raw.mystore);
                try {
                    trusted.load(in, "MyStorePassword");
                } finally {
                    in.close();
                }
                return new SSLSocketFactory(trusted);
            } catch (Exception e) {
                throw new AssertionError(e);
            }
        }
    }
}

Let's break the example in some munchable parts:


  • We created  a MyRestService class which facilitates the execution of POST, GET, etc methods to your endpoint;
  • Instead of using the standard HttpClient, we created a MyHttpClient which extends from DefaultHttpClient.
    • When a connection is created, it will register our custom SSLSocketFactory class when accessing HTTPS resources;
    • When the SSLSocketFactory is created, we load the keystore that contains the certificate using a password (in this case MyStorePassword).
  • You can now use the MyHttpClient as you would with a normal DefaultHttpClient instance. Only if a connection is made your REST service, it will load the appropriate certificate.
How you can build the KeyStore, stored in /raw/mystore is explained in the article from Crazy Bob's blog: http://blog.crazybob.org/2010/02/android-trusting-ssl-certificates.html.

Geen opmerkingen:

Een reactie plaatsen