4

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...

LightCC
  • 9,804
  • 5
  • 52
  • 92
Tal Kanel
  • 10,475
  • 10
  • 60
  • 98
  • I have a couple of follow up questions. Are we dealing with redirects? For Man in the Middle, is it enough to restrict your app to only access websites with https only? You'd be able to accomplish that with some of the options on https://developer.android.com/training/articles/security-config - though I know many people who are against certificate pinning, pls consider any other option from the link. – Fabio Jul 17 '20 at 07:47
  • Let's say I do not have redirects, and all the downloads are https. can you explain why setting security-config prevents man in the middle attack? Also, download manager is another process, so he couldn't react to any security-config file defined in my own app.. – Tal Kanel Jul 19 '20 at 05:30
  • If your device doesn't have rogue root ssl certificates you can assume all https communication is safe agains MiM. Thus by blocking http traffic you can assume you're safe. I'm keen to hear conter arguments. – Fabio Jul 21 '20 at 23:55

1 Answers1

1

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.

Touhid
  • 731
  • 1
  • 10
  • 25
  • Thanks @Touhid. I know how to write code that doing this certificate signature check when the request done from my own process. I know also that I can verify the file after download, and I'm doing so. but, it's not enough: I want to prevent from the file from been downloaded in the first place. this is not enough for me, and I prefer to keep on using download manager instead of writing my own download manager. – Tal Kanel Jul 19 '20 at 11:37
  • Then I am pretty sure this is not doable from Android side using `DownloadManager`, but probably from the file-storage part like IAM control on AWS S3 contents [https://aws.amazon.com/blogs/security/writing-iam-policies-grant-access-to-user-specific-folders-in-an-amazon-s3-bucket/]. – Touhid Jul 19 '20 at 11:42