Включаем поддержку TLS 1.1+ на Android 4.1-4.4

image alt >

Что есть TLS?

Если вкратце, то это протокол, который обеспечивает защищенное соединение между узлами (клиентом и сервером, например) всемирной паутины. Более подробно TLS рассмотрен в статье на Хабре.

В каких случаях это может понадобиться?

В любых случаях работы с безопасным соединением. Самый примитивный пример — чтение веб-контента сайта, работающего с TLS, в строку. Это может потребоваться, например, для работы с API или для любых других нужд.

Как узнать, какой из безопасных протоколов используется на сайте?

image alt > Самый простой способ — зайти на сайт с домашнего браузера. Если в адресной строке появляется «замок», значит сайт использует безопасное соединение. Нажав на этот «замок», можно узнать более подробную информацию.

На примере видно, что сайт vk.com использует протокол TLS версии 1.2.

TLS и версии Android

Как гласит нам документация, «из коробки» TLS версии 1.1+ работает лишь на Android версии 5.0 и выше (API 20+).

TLS и Android 2.3-4.4

Давайте таки напишем простейший код чтения сайта в строку и запустим его на младших версиях Android.

   public String readFromURL(String url) throws IOException {
        StringBuilder scanned = new StringBuilder();
        java.net.URL pageURL = new URL(url);
        HttpsURLConnection uc = (HttpsURLConnection) pageURL.openConnection();
        BufferedReader br = new BufferedReader(new InputStreamReader(uc.getInputStream(), "UTF-8"));

        String inputLine;
        while ((inputLine = br.readLine()) != null) {
            scanned.append(inputLine);
        }

        br.close();
        return scanned.toString();
    }

Запустив его на Android-девайсе или эмуляторе, получим ошибку SSLException: connection closed by peer.

Решение проблемы

Решение возникшей проблемы, оказывается, очень простое, но работает только под версии 4.1 и старше (API 16+). Это связано с тем, что до 4.1 android попросту не знал о существовании TLS 1.1+.

Мы просто создадим новую SSLSocketFactory, где подключим к сокету нужные нам протоколы, которые по умолчанию не включены в Android 4.1-4.4.

Итак, создадим класс TLSSocketFactory со следующим содержимым:

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

public class TLSSocketFactory extends SSLSocketFactory {

    private SSLSocketFactory mSSLSocketFactory;

    public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, null, null);
        mSSLSocketFactory = context.getSocketFactory();
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return mSSLSocketFactory.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return mSSLSocketFactory.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        return enableTLS(mSSLSocketFactory.createSocket(s, host, port, autoClose));
    }

    @Override
    public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        return enableTLS(mSSLSocketFactory.createSocket(host, port));
    }

    @Override
    public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        return enableTLS(mSSLSocketFactory.createSocket(host, port, localHost, localPort));
    }

    @Override
    public Socket createSocket(InetAddress host, int port) throws IOException {
        return enableTLS(mSSLSocketFactory.createSocket(host, port));
    }

    @Override
    public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        return enableTLS(mSSLSocketFactory.createSocket(address, port, localAddress, localPort));
    }

    private Socket enableTLS(Socket socket) {
        if(socket != null && (socket instanceof SSLSocket)) {
            String[] protocols = new String[] {
                    "TLSv1.1",
                    "TLSv1.2"
            };
            ((SSLSocket) socket).setEnabledProtocols(protocols);
        }
        return socket;
    }
}

В методе enableTLS() мы прописываем конкретные протоколы, которые хотели бы использовать в нашем подключении.

Теперь изменим код метода readFromURL(), назначив нашему соединению HttpsURLConnection вновь созданную «фабрику»: uc.setSSLSocketFactory(new TLSSocketFactory());

Замечание: в данном случае будут использоваться исключительно те протоколы, которые вы пропишете. Если необходимо использовать данное соединение с другими протоколами, не забудьте их указать.

Итак, полностью рабочий код:

public String readFromURL(String url) throws IOException, NoSuchAlgorithmException, KeyManagementException {
    StringBuilder scanned = new StringBuilder();
    java.net.URL pageURL = new URL(url);
    HttpsURLConnection uc = (HttpsURLConnection) pageURL.openConnection();
    uc.setSSLSocketFactory(new TLSSocketFactory());
    BufferedReader br = new BufferedReader(new InputStreamReader(uc.getInputStream(), "UTF-8"));

    String inputLine;
    while ((inputLine = br.readLine()) != null) {
        scanned.append(inputLine);
    }

    br.close();
    return scanned.toString();
}