0

I have an Android app using web services from an external server (that I do not control). Recently that server failed to renew its HTTPS certificate, and was unavailable for a few hours. During this time interval, a few users of my app attempted to use the services, which naturally failed. The problem is that now that the problem is fixed on the server, these users are still unable to access the website from my app. One user can't even access the website from his mobile device's browser, another one is only blocked when trying from my app.

I have limited experience with HTTPS certificates renewing, so I'd like to know what could be wrong? It seems like these devices have kept in cache the expired certificate, and do not take the new one. Reinstalling my app doesn't fix the problem.

Thanks

Tim Autin
  • 6,043
  • 5
  • 46
  • 76
  • If your app has enabled network security configuration, ensure that your new certificate is covered by whatever network security configuration rules you have set up. However, the "can't even access the website from his mobile device's browser" issue suggests that the server team perhaps chose an unconventional certificate authority, one that not all Android versions necessarily recognize. – CommonsWare Aug 08 '19 at 15:55
  • Thanks. Well if there was a problem with the app network security config, all the users would be affected, right? I'll ask them if they changed the certificate authority. – Tim Autin Aug 08 '19 at 17:25
  • "if there was a problem with the app network security config, all the users would be affected, right?" -- only those on Android 7.0 and higher, as network security configuration did not exist before then. But yes, within that subset, I would expect problems. – CommonsWare Aug 08 '19 at 17:30
  • Yes, so that's not the problem. I'm running the app under Android 9 without any problem, as well as 25k users using Android 4 to 9. Only two users have reported the bug. Can't there be a location on the device holding some kind of cache? – Tim Autin Aug 08 '19 at 18:43
  • Not that I am aware of. OTOH, some device manufacturer may have done something odd. What are the models and OS versions of the two problematic devices? – CommonsWare Aug 08 '19 at 18:46
  • I need to ask them, I'll tell you once they have answered. – Tim Autin Aug 08 '19 at 18:49
  • I think the problem is that unencrypted (http) traffic is being blocked by default in the most recent version of the OS. You can tell it to override that block, but that is not a very secure option. But, one you might check out if necessary. To override, you can add the flag android:usesCleartextTraffic=”true” to the AndroidManifest.xml file. – Michael Dougan Aug 08 '19 at 20:32
  • @Michael, thanks but if that was the case, all devices would be affected. Moreover I'm using HTTPS. – Tim Autin Aug 09 '19 at 14:32
  • It depends on what version of the OS the device is running. Also affected is the version of Chrome running on the device, as the Chrome client is used by things like WebViews as a base, and Chrome only recently started refusing http traffic. And, while you may be using HTTPS, if the server you hit doesn't have a valid HTTPS certificate, you're going to be redirected to HTTP. It's easy enough to add that flag to your AndroidManifest.xml and test it out, if it doesn't do anything, then you've ruled that out at least! – Michael Dougan Aug 09 '19 at 15:52

1 Answers1

1

I finally found a solution, thanks to Javax.net.ssl.SSLHandshakeException: javax.net.ssl.SSLProtocolException: SSL handshake aborted: Failure in SSL library, usually a protocol error .

First download the HTTPS certificate of the problematic website (I used Firefox to do it) and put it in your assets folder. Then extend Application, and add the following:

public class MyApplication extends Application {

    private static SSLSocketFactory _sslSocketFactory = null;

    @Override
    public void onCreate() {

        super.onCreate();

        installSslIfNeeded();
        loadSslSocketFactoryIfNeeded();
    }

    @Nullable
    public static SSLSocketFactory getSslSocketFactory() {
        return _sslSocketFactory;
    }

    private void installSslIfNeeded() {

        // Install SSL certificates if needed:
        // See: https://stackoverflow.com/questions/29916962/javax-net-ssl-sslhandshakeexception-javax-net-ssl-sslprotocolexception-ssl-han
        try {
            ProviderInstaller.installIfNeeded(this);
            SSLContext sslContext;
            sslContext = SSLContext.getInstance("TLSv1.2");
            sslContext.init(null, null, null);
            sslContext.createSSLEngine();
        }
        catch (GooglePlayServicesRepairableException | GooglePlayServicesNotAvailableException | NoSuchAlgorithmException | KeyManagementException e) { e.printStackTrace(); }

    }

    private void loadSslSocketFactoryIfNeeded() {

        // Create a static SSL factory trusting the server's HTTPS certificate whose authority
        // is unknown for Android < 5
        // https://developer.android.com/training/articles/security-ssl

        if (_sslSocketFactory == null) {

            try {

                // Load certificate:
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                InputStream caInput = getAssets().open("theserver.crt");
                Certificate ca;
                //noinspection TryFinallyCanBeTryWithResources
                try { ca = cf.generateCertificate(caInput); }
                finally { caInput.close(); }

                // Create a KeyStore containing our trusted CAs
                KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
                keyStore.load(null, null);
                keyStore.setCertificateEntry("ca", ca);

                // Create a TrustManager that trusts the CAs in our KeyStore
                String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
                TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
                tmf.init(keyStore);

                // Create an SSLContext that uses our TrustManager
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, tmf.getTrustManagers(), null);

                _sslSocketFactory = sslContext.getSocketFactory();
            }
            catch (CertificateException e) { e.printStackTrace(); }
            catch (NoSuchAlgorithmException e) { e.printStackTrace(); }
            catch (KeyStoreException e) { e.printStackTrace(); }
            catch (IOException e) { e.printStackTrace(); }
            catch (KeyManagementException e) { e.printStackTrace(); }
        }
    }
}

Now, if you want for instance to download a JSON file from a REST API, you can do it this way:

static JSONObject readJsonFromUrl(String urlString) throws IOException, JSONException {

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    URL url = new URL(urlString);
    HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
    urlConnection.setSSLSocketFactory(MyApplication.getSslSocketFactory());

    return readJsonFromInputStream(urlConnection.getInputStream());
}

If you're using a webview, you need to do the following:

_webview.setWebViewClient(new WebViewClient() {

    [...]

    @Override
    public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {

        final AlertDialog.Builder builder = new AlertDialog.Builder(MyActivity.this);
        builder.setMessage(R.string.my_ssl_error_message);

        builder.setPositiveButton(R.string.common_continue, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                handler.proceed();
            }
        });

        builder.setNegativeButton(R.string.common_cancel, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                handler.cancel();
            }
        });

        builder.show();
    }
});
Tim Autin
  • 6,043
  • 5
  • 46
  • 76