androidssl-certificateandroid-download-managercertificate-pinning

Is it possible to restrict DownloadManager.Request to a specific server certificate?


As part of my effort to improve my application's security, I wanted to protect my client from "Man in the Middle" attacks.

I've got a common use-case in which my app downloads large files (10-50 mega) from the CDN server. To do that - I'm using the System's DownloadMnager

Is there a way to set any specific TrustManager or specific server certificate key via it's API? Is there any other way to pin the request to a specific trusted server?

Looks like there's no such API, but I will be surprised if that's really the case because GooglePlay using the System's download manager to download apk's and then install them...


Solution

  • Being a systemwide usable API, I doubt there can be any possible way to restrict DownloadManager to a specific server-certificate. In response to your mentioned example, Google-Play most probably is installing the downloaded APK by observing the download-completion.

    But if my understanding is right, you can achieve your target by using Retrofit library's file-download method as discussed in this SO post, while the certificate-pinning can be achieved by using the following SelfSigningClientBuilder class to build the Retrofit client:

    SelfSigningClientBuilder.kt

    import android.content.Context
    import okhttp3.Interceptor
    import okhttp3.OkHttpClient
    import okhttp3.Request
    import java.io.IOException
    import java.security.*
    import java.security.cert.CertificateException
    import java.security.cert.CertificateFactory
    import java.security.cert.X509Certificate
    import java.util.*
    import java.util.concurrent.TimeUnit
    import javax.net.ssl.*
    
    object SelfSigningClientBuilder {
    
        private const val NET_TIMEOUT_READ = 80L
        private const val NET_TIMEOUT_WRITE = 120L
        private const val NET_TIMEOUT_CONNECT = 75L
    
        @JvmStatic
        fun createClient(context: Context, isCertificateNeeded: Boolean = true): OkHttpClient {
            val interceptor = getInterceptor()
            if (isCertificateNeeded)
                try {
                    val cf = CertificateFactory.getInstance("X.509")
                    // assuming the CA certificate is put inside res/raw folder named as ca_cert.pem
                    val cert = context.resources.openRawResource(R.raw.ca_cert)
                    val ca = cf?.generateCertificate(cert)
                    cert.close()
                    val keyStoreType = KeyStore.getDefaultType()
                    val keyStore = KeyStore.getInstance(keyStoreType)
                    keyStore.load(null, null)
                    keyStore.setCertificateEntry("ca", ca)
                    val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
                    val tmf = TrustManagerFactory.getInstance(tmfAlgorithm)
                    tmf.init(keyStore)
                    val trustManagers = tmf.trustManagers
                    if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) {
                            throw IllegalStateException("Unexpected default trust managers:"
                            + Arrays.toString(trustManagers))
                    }
                    val trustManager = trustManagers[0] as X509TrustManager
                    val sslContext = SSLContext.getInstance("SSL")
                    sslContext!!.init(null, trustManagers, null)
    
                    return OkHttpClient.Builder()
                        .sslSocketFactory(sslContext.socketFactory, trustManager)
                        .readTimeout(NET_TIMEOUT_READ, TimeUnit.SECONDS)
                        .writeTimeout(NET_TIMEOUT_WRITE, TimeUnit.SECONDS)
                        .connectTimeout(NET_TIMEOUT_CONNECT, TimeUnit.SECONDS)
                        // .retryOnConnectionFailure(true)
                        .addInterceptor(interceptor)
                        .build()
                } catch (e: KeyStoreException) {
                    e.printStackTrace()
                } catch (e: CertificateException) {
                    e.printStackTrace()
                } catch (e: NoSuchAlgorithmException) {
                    e.printStackTrace()
                } catch (e: IOException) {
                    e.printStackTrace()
                } catch (e: KeyManagementException) {
                    e.printStackTrace()
                }
            return OkHttpClient.Builder()
                .readTimeout(NET_TIMEOUT_READ, TimeUnit.SECONDS)
                .writeTimeout(NET_TIMEOUT_WRITE, TimeUnit.SECONDS)
                .connectTimeout(NET_TIMEOUT_CONNECT, TimeUnit.SECONDS)
                .addInterceptor(interceptor)
                .build()
        }
    
        private fun getInterceptor(): Interceptor {
            return Interceptor { chain ->
                val originalRequest = chain.request()
                val request: Request = originalRequest.newBuilder()
                    .header("custom-header", "my-header-value")
                    .method(originalRequest.method(), originalRequest.body())
                    .build()
                chain.proceed(request)
            }
        }
    }
    

    Then construct the Retrofit-client following the code-segment here and then use it to download the file:

    val retrofit = Retrofit.Builder()
                .client(SelfSigningClientBuilder.createClient(context, true)
                .baseUrl("https://yourdomain.com/")
                .build()
    

    While using this retrofit client, I have parsed all of my REST API requests by Wireshark, Burp-suite, and Charles Proxy - none of which could show the actual request text instead of some gibberish data. So, I hope your file-content will be protected from MITM attack while following this process.