在Android 4.1+中强制开启TLSv1.1和TLSv1.2

在做MQTT时遇到了android4.4手机没法连接服务端的情况,查阅发现是因为系统默认没有开启TLSv1.1和TLSv1.2的支持。抛出的错误如下:

1
2
3
 MqttException (0) – javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: ssl=0x7861ada8: Failure in SSL library, usually a protocol error
 
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version (external/openssl/ssl/s23_clnt.c:741 0x71e78cf8:0x00000000)

Android 4.1开始支持TLSv1.1和TLSv1.2,但是在4.1~4.4版本中默认都是关闭。在某些情况下可能会导致SSL链接建立失败的问题(服务端强制使用1.1和/或1.2,而android 4.1~4.4中没有开启。),这时需要开启TLSv1.1和TLSv1.2。

我们需要使用自定义的SSLSocketFactory去创建SSL链接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class CustomSslSocketFactory extends SSLSocketFactory {
 
    private SSLSocketFactory sslSocketFactory;
 
    public CustomSslSocketFactory(TrustManager[] trustManager) throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, trustManager, null);
        sslSocketFactory = context.getSocketFactory();
    }
 
    @Override
    public String[] getDefaultCipherSuites() {
        return sslSocketFactory.getDefaultCipherSuites();
    }
 
    @Override
    public String[] getSupportedCipherSuites() {
        return sslSocketFactory.getSupportedCipherSuites();
    }
 
    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(s, host, port, autoClose));
    }
 
    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port, localHost, localPort));
    }
 
    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
    }
 
    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket(address, port, localAddress, localPort));
    }
 
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }
 
    private Socket enableTLSOnSocket(Socket socket) {
        ((SSLSocket) socket).setEnabledProtocols(new String[] {"TLSv1.2", "TLSv1.1"});
        return socket;
    }
}

需要注意的是如果遇到如下错误,那需要override一个空参数的createSocket()方法。

1
2
3
4
5
6
7
Error:java.net.SocketException: Unconnected is not implemented 
 
//Override createSocket()
    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(sslSocketFactory.createSocket());
    }

使用这个自定义的SSLSocketFactory类就可以去做TLSv1.1和TLSv1.2的SSL链接就可以了。

Retrofit/OkHttp SSL Pining

这周在做竞品研究的时候发现,只要安装抓包工具的相关CA证书,就可以看到SSL加密的数据了。为了防止request结构被其他人看到,决定使用SSL Pinning,使得App只认绑定证书。这样即便安装了抓包工具的证书,也会因为证书的key不对而不会建立链接。

OkHttp提供了一个CertificatePinner类可以方便的设置SSL Pinning,但实验下来发现在Android中不行。使用抓包工具还是可以看到加密内容。或许是我的使用方法有问题。不过我还是找另外一种方法做SSL Pinning。

这里使用的方法是从CA证书中读取相关的信息,并创建一个只信任指定CA证书的SSLSocketFactory对象,注入到OkHttp中。这样OkHttp会使用注入的SSLSocketFactory去创建SSL Socket了。

第一步提取CA证书(以Google为例):

Get Google CA
Get Google CA
Click “View Certificate”
Click “Export”

导出后得到一个后缀名为.crt的文件,该文件既是Google的CA证书。

 

第二步读取CA证书并创建创建SSLSocketFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SSLSocketFactory = sslSocketFactory = null;
try {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
 
    InputStream caInput = context.getResources().openRawResource(R.raw.ca);
    Certificate ca = null;
    try {
        ca = certificateFactory.generateCertificate(caInput);
    } catch (CertificateException e) {
        e.printStrackTrace();
    } finally {
        caInput.close();
    }
 
    String keyStoreType = KeyStore.getDefaultType();
    KeyStore keyStore = KeyStore.getInstance(keyStoreType);
    keyStore.load(null, null);
    if (ca == null) {
        return null;
    }
    keyStore.setCertificateEntry("ca", ca);
 
    String algorithm = TrustManagerFactory.getDefaultAlgorithm();
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
    trustManagerFactory.init(keyStore);
 
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
 
    sslSocketFactory = sslContext.getSocketFactory();
} catch (CertificateException|IOException|KeyStoreException|NoSuchAlgorithmException|KeyManagementException e) {
    e.printStackTrace();
}

 

第三步在创建OkHttpClient时将SSLSocketFactory注入到Builder:

1
2
3
OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(SslSocketFactoryHelper.getSslSocketFactory(context.get()))
        .build();

这样就大功告成了。

再测试一下会发现因为CA证书不对,链接会无法建立。也就达到了SSL Pinning的效果。

Retrofit 2简单入门

Retrofit是一个Android/Java的type-safe HTTP客户端,它能够大大简化写网络请求的复杂程度。而之前发布的2.0版本改动非常大。1.x版本默认使用URLConnection作为底层Http Client,而2.0版本开始只能使用OkHttpClient作为底层Http Client。而数据格式的Converter配置也发生了变化。这篇文章做一个简单的入门小结。

首先在Android Studio中引入Retrofit 2以及相关的依赖库:

1
2
3
4
5
    compile 'com.squareup.okhttp3:okhttp:3.2.0'
    compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
 
    compile 'com.squareup.retrofit2:retrofit:2.0.0'
    compile 'com.squareup.retrofit2:converter-moshi:2.0.0'

这里是用Moshi converter库作为Converter例子。

首先创建一个最简单的HTTP客户端:

1
    Retrofit retrofit = new Retrofit.Builder().baseUrl("http://www.example.com/").build();

1.x中使用endPoint(String)方法指定endPoint,在2.0中改成了baseUrl(String)方法。

创建一个使用Moshi converter的HTTP客户端:

1
2
3
4
    Retrofit retrofit = new Retrofit.Builder()
                                .baseUrl("http://www.example.com/")
                                .addConverterFactory(MoshiConverterFactory.create())
                                .build();

使用这个客户端收到的返回消息的body都会使用Moshi Json库去做反序列化过程。

当我们所有的请求头部都有指定字段时,我们可以先创建一个定制的OkHttpClient对象,再把这个对象设置为Retrofit的底层Http client就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    OKHttpClient client = new OkHttpClient.Builder();
    client.addInterceptor(new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request().newBuilder()
                    .addHeader("Context-Type", "application/json").build();
            return chain.proceed(request);
        }
    });
 
    Retrofit retrofit = new Retrofit.Builder()
                                .client(client)
                                .baseUrl("http://www.example.com/")
                                .build();

下面我们创建一个Api接口:

1
2
3
4
5
    interface Api {
        @GET("api/example/{uuid}")
        Call<void> getInfo(@Path("uuid") String uuid);
    }
</void>

发起请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    Retrofit retrofit = new Retrofit.Builder().baseUrl("http://www.example.com/").build();
    Api api = retrofit.create(Api.class);
    Call<void> getInfo = api.getInfo("uuid");
    getInfo.enqueue(new Callback</void><void>() {
        @Override
        public void onResponse(Call</void><void> call, Response</void><void> response) {
            if (response.isSuccessful() {
                // server says success
            } else {
                // server says failure
            }
        };
 
        @Override
        public void onFailure(Call</void><void> call, Throwable t) {
            // request failed. network error or something else.
        }
    });
</void>

我们通过enqueue(Callback)方法发起请求并且获得结果。Callback接口中的onResponse方法代表服务器成功收到了请求并且返回了结果,我们需要通过Response.isSuccessful()方法得知返回的结果是否正确(也就是说是否按照Converter成功反序列化了),如果其返回为false,则可能是反序列化失败活着服务器返回了400等其它错误。而Callback.onFailure方法代表请求发送失败,也就是说请求没有发送到服务器,一般是Api接口写的有问题,这个需要检查接口的定义了。

Retrofit使用起来非常方便。当然它还支持非常好的自定义功能,这个需要进一步探索。