【Android】讓Android 4.4支援TLSv1.3

想不到吧,在2022年的這個時空竟然還有文章要寫關於Android 4.4的相關議題,現在 Android 都來到了 13 了。儘管在新裝置上 Android 4.4 已然銷聲匿跡,但打開家中那些白牌的機上盒,插在電視 USB 孔上的電視棒,還有許多裝置上面跑的是 Android 4.4 的版本。在當時的時空下,使用 Android 4.4 當然是沒有什麼問題的,但物換星移,來到了現在的時空,各大網站的安全性提升,原本的 SSL 也早已變成了 TLS,版本還來到了 1.3,若不針對 Android 4.4 加以處理,那麼則會因為不支援加密規格的問題,而衍生出許多連線失敗的狀況,最主要就是因為 Android 4.4 並不支援 TLSv1.3 版本。

Android 的加密

在 Java 中,Java Security Provider (JSP) 包含了Java Cryptography Extension (JCE) 和 Java Secure Socket Extension (JSSE)。 而在 Android 4.4裡,負責這種加解密的是 openssl 這個套件,但 TLSv1.3 要到 1.0.2j 這個版本才支援,而 Android 4.4裡面的版本是 1.0.1e 那麼自然是沒半法支援到 TLSv1.3 的。

GMS Security Provider

如果很幸運的,你的目標裝置裡面有 Google Play Service 而且系統版本是 Android 5.0 以上,那麼你可以很輕鬆地使用 Google 爸爸所提供的套件來解決這個問題。

implementation 'com.google.android.gms:play-services-basement:18.1.0'
ProviderInstaller.installIfNeededAsync(this, new ProviderInstallListener() {
    @Override
    public void onProviderInstalled() {}
    
    @Override
    public void onProviderInstallFailed(int errorCode, Intent recoveryIntent) {
        GooglePlayServicesUtil.showErrorNotification(errorCode, MainApplication.this);
    }
});

但很抱歉,通常白牌機上盒都沒有 Google Play Service,且我們的目標裝置版本是 Android 4.4,所以死了這條心,另尋方法吧 🫠

替換新版 Conscrypt

Conscrypt 是 Android 內建的 JSP,Google 將其獨立了出來,也就是說我們可以透過用套件更新的方式來使用新版的 Conscrypt。以下範例請搭配 OKHttp 服用:

// Android 4.1+
dependencies {
	implementation 'com.squareup.okhttp3:okhttp:3.12.13'
	implementation 'org.conscrypt:conscrypt-android:2.5.2'
}

// Android 5.0+
dependencies {
	implementation 'com.squareup.okhttp3:okhttp:4.10.0'
	implementation 'org.conscrypt:conscrypt-android:2.5.2'
}
// Init Conscrypt
Provider conscrypt = Conscrypt.newProvider();

// Add as provider
Security.insertProviderAt(conscrypt, 1);

// Init OkHttp
OkHttpClient.Builder okHttpBuilder = 
    new OkHttpClient().newBuilder()
        .connectionSpecs(
            Arrays.asList(
                ConnectionSpec.RESTRICTED_TLS,
                ConnectionSpec.COMPATIBLE_TLS,
                ConnectionSpec.MODERN_TLS,
                ConnectionSpec.CLEARTEXT // Support Http
            )
        );

// OkHttp 3.12.x
// ConnectionSpec.COMPATIBLE_TLS = TLS1.0
// ConnectionSpec.MODERN_TLS = TLS1.0 + TLS1.1 + TLS1.2 + TLS 1.3
// ConnectionSpec.RESTRICTED_TLS = TLS 1.2 + TLS 1.3

// OkHttp 3.13+
// ConnectionSpec.COMPATIBLE_TLS = TLS1.0 + TLS1.1 + TLS1.2 + TLS 1.3
// ConnectionSpec.MODERN_TLS = TLS1.2 + TLS 1.3
// ConnectionSpec.RESTRICTED_TLS = TLS 1.2 + TLS 1.3

try {
	X509TrustManager tm = Conscrypt.getDefaultX509TrustManager();
	SSLContext sslContext = SSLContext.getInstance("TLS", conscrypt);
	sslContext.init(null, new TrustManager[] { tm }, null);
	okHttpBuilder.sslSocketFactory(
        new InternalSSLSocketFactory(sslContext.getSocketFactory()),
        tm
    );
} catch (Exception e) {
	e.printStackTrace();
}
  
// Build OkHttp
OkHttpClient okHttpClient = okHttpBuilder.build();
public final class InternalSSLSocketFactory extends SSLSocketFactory {
    private final SSLSocketFactory mSSLSocketFactory;

    public InternalSSLSocketFactory(
        SSLSocketFactory sslSocketFactory) {
        this.mSSLSocketFactory = sslSocketFactory;
    }

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

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

    @Override
    public Socket createSocket() throws IOException {
        return enableTLSOnSocket(mSSLSocketFactory.createSocket());
    }

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

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

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

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

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

    private Socket enableTLSOnSocket(Socket socket) {
        if (socket instanceof SSLSocket) {
        	((SSLSocket) socket).setEnabledProtocols(
                new String[] { "TLSv1.2", "TLSv1.3" }
            );
        }
        return socket;
    }
}

資料來源
https://enzowyf.github.io/android_alpn.html
https://gist.github.com/Karewan/4b0270755e7053b471fdca4419467216
https://developer.android.com/training/articles/security-gms-provider
https://github.com/google/conscrypt
https://xiaopc.org/2022/10/07/如何让应用在-android-4-4-下使用-tlsv1-3/