bugdroid.png

As described by the PCI Security Standards Council, early versions of the TLS security protocol are susceptible to widespread exploits that could expose potentially sensitive data. On June 30th, we upgraded Quizlet's security to require TLS 1.2 or higher as part of our continued following of best-practice data security recommendations.

While our web app and iOS clients were largely unaffected by this transition, we had to do a bit of work to make sure our Android client was up to the job.

This was due to a few reasons:

  • numbered text There is a lack of clarity surrounding TLS 1.2 support on older Android devices,
  • Device manufacturers have differing commitments to the official Android specs for shipping TLS 1.2 on their devices
  • Carriers and device manufacturers have differing commitments to providing software and security updates to their customers

There's unfortunately no simple way to ensure all of our users are seamlessly able to use TLS 1.2 to access Quizlet, but we've tried our best to minimize disruption to our users as we made the transition.

In this post, I want to share how we went about doing it.

Installing TLS 1.2 Where Needed

We support Android 4.1–4.3 (API 16–18) with a long-term-support version of our app, and Android 4.4+ (API 19+) in our latest builds. Our goal was to minimize disruption for users in both of those builds, so our target was ensuring TLS 1.2 support for Android 4.1 and higher.

The first thing we realized was that despite documentation suggesting otherwise, not all devices on Android 4.1+ actually support TLS 1.2. Even though it is likely due to device manufacturers not fully following the official Android specs, we had to do what we could to ensure this would work for our users.

Luckily, Google Play Services provides a way to do this. The solution is to use ProviderInstaller from Google Play Services to try to update the device to support the latest and greatest security protocols.

We do this as follows:

fun Context.installTls12() {
  try {
      ProviderInstaller.installIfNeeded(this)
  } catch (e: GooglePlayServicesRepairableException) {
      // Prompt the user to install/update/enable Google Play services.
      GoogleApiAvailability.getInstance()
          .showErrorNotification(this, e.connectionStatusCode)
  } catch (e: GooglePlayServicesNotAvailableException) {
      // Indicates a non-recoverable error: let the user know.
  }
}

For devices without Google Play services, we unfortunately don't have an alternative way to patch the device's security Provider to support TLS 1.2. For these users, we display a message so they know the app may not work properly for them.

In the Quizlet Android app, we call this in a background thread if our first API call fails with an SSLHandshakeException. Calling ProviderInstaller.installIfNeeded only after an SSLException allows us to support users with TLS 1.2 already installed with minimal disruption (i.e., if they have an old version of Google Play Services, or don't have it installed at all). In your apps, it might make sense to handle this logic in a similar manner.

Alternatives to this approach include Google's official suggestions: either blocking network transactions by calling installIfNeeded upon creation of all network-specific threads (it's a no-op if the security Provider is already up-to-date, so the performance impact should be minimal), or using the asynchronous variant to start a call from the main thread.

Originally, we thought we were done after doing this step. However, after further testing, we noticed something peculiar. Many devices succeeded in updating the security Provider via installIfNeeded, but were still throwing SSLHandshakeExceptions when the server required TLS 1.2!

Forcing TLS 1.2 To Be Enabled

Our initial understanding of OkHttp's HTTPS strategy was that it uses the highest-installed version of TLS, and it falls back to older protocols if newer ones are not installed. This is false.

OkHttp's default behavior is to use an SSLSocketFactory to create SSLSockets with default parameters, and select from protocols that are enabled for a given SSLSocket. Here's the kicker: The latest protocols installed on a device are not necessarily enabled on its default SSLSockets!

The docs for SSLSocket on Android state that TLS 1.2 is only enabled as a default client protocol starting in Android 4.3. To make things more confusing, developers have found that certain Samsung devices on Android 4.4 don't have it enabled either.

To fix this, we need to override OkHttp's SSLSocketFactory to enable TLS 1.2 on all SSLSockets. We adapted the helper class from this Github issue, and cleaned it up a bit to make it more Kotlin-tastic. We cleared up deprecation warnings by explicitly passing in an X509TrustManager and made use of Kotlin's extension functions to simplify some of the code, but the actual logic is largely the same.

The new-and-improved Tls12SocketFactory is a bit too long to embed, but here's a link.

Now, we can simply call the enableTls12() extension function in our OkHttpClient.Builder chain in Dagger, and there we have it!
Or so we thought.

Forcing TLS 1.2 To Be Enabled, Take 2: Glide

It turns out this still wasn't enough! Our regular-old API calls were working just fine, but all of our images were still failing to load through Glide due to even more SSLHandshakeExceptions.

By default, the Glide OkHttp3 integration uses a vanilla OkHttpClient to load images from URLs. This means it uses the regular SSLSocketFactory that does not forcefully enable TLS 1.2.

Lucky for us, this fix wasn't too complicated for Glide. We set up our AppGlideModule to use our Tls12SocketFactory for its OkHttpClient as well:

@GlideModule
class QuizletGlideModule : AppGlideModule() {
  override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
    super.registerComponents(context, glide, registry)

    val okHttpClient = OkHttpClient.Builder()
      .enableTls12()
      .build()

    registry.replace(
      GlideUrl::class.java,
      InputStream::class.java,
      OkHttpUrlLoader.Factory(okHttpClient)
    )
  }

  override fun isManifestParsingEnabled() = false
}

If you use Glide or any other libraries that manage their own network calls, you should make sure that their SSLSockets are also configured to enable TLS 1.2.
Whew. Now we're done!

In Summary

A device can be in one of several states with regards to supporting TLS 1.2:

  1. Device does not have TLS 1.2 installed at all.
  2. numbered textDevice has TLS 1.2 installed, but not enabled by default.
  3. numbered textDevice has TLS 1.2 installed and enabled by default.

It's important to make sure you handle not only installing TLS 1.2 support (via ProviderInstaller) but also ensuring that it's enabled for all of your network connections - even ones that are handled automagically by other libraries!

[The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License. Special thanks to Damien Diehl and Eric Suesz for their help!]