Skip to content

Commit d8ec6dd

Browse files
committed
Provide a clear manual cert setup flow for Android 11
1 parent 78d8d87 commit d8ec6dd

File tree

3 files changed

+81
-12
lines changed

3 files changed

+81
-12
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ dist: trusty
33
android:
44
components:
55
- build-tools-29.0.3
6-
- android-29
6+
- android-30
77
before_install:
88
- openssl aes-256-cbc -K $encrypted_410c058128c0_key -iv $encrypted_410c058128c0_iv
99
-in android-travis-cert-store.enc -out ./android-travis-cert-store.jks -d
1010
script:
11+
- yes | sdkmanager "platforms;android-30" # Accept the Android 11 platform license, since Travis can't yet
1112
- "./gradlew assembleRelease"
1213
- jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -storepass $CERT_STORE_PWD
1314
-keypass $CERT_STORE_PWD -keystore ./android-travis-cert-store.jks app/build/outputs/apk/release/app-release-unsigned.apk

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
33
apply plugin: 'kotlin-android-extensions'
44

55
android {
6-
compileSdkVersion 29
6+
compileSdkVersion 30
77
buildToolsVersion "29.0.3"
88

99
defaultConfig {

app/src/main/java/tech/httptoolkit/android/MainActivity.kt

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ package tech.httptoolkit.android
22

33
import android.content.*
44
import android.content.pm.PackageManager
5-
import android.content.res.Configuration
65
import android.net.Uri
76
import android.net.VpnService
87
import android.os.Build
98
import android.os.Bundle
9+
import android.os.ParcelFileDescriptor
1010
import android.os.PowerManager
11+
import android.provider.MediaStore
1112
import android.provider.Settings
1213
import android.security.KeyChain
1314
import android.security.KeyChain.EXTRA_CERTIFICATE
@@ -17,6 +18,7 @@ import android.view.View
1718
import android.widget.Button
1819
import android.widget.LinearLayout
1920
import android.widget.TextView
21+
import androidx.annotation.RequiresApi
2022
import androidx.annotation.StringRes
2123
import androidx.appcompat.app.AppCompatActivity
2224
import androidx.appcompat.view.ContextThemeWrapper
@@ -25,6 +27,8 @@ import com.google.android.gms.common.GooglePlayServicesUtil
2527
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2628
import io.sentry.Sentry
2729
import kotlinx.coroutines.*
30+
import java.lang.RuntimeException
31+
import java.security.cert.Certificate
2832
import java.security.cert.X509Certificate
2933

3034

@@ -43,6 +47,8 @@ enum class MainState {
4347
private const val ACTIVATE_INTENT = "tech.httptoolkit.android.ACTIVATE"
4448
private const val DEACTIVATE_INTENT = "tech.httptoolkit.android.DEACTIVATE"
4549

50+
private val PROMPTED_CERT_SETUP_SUPPORTED = Build.VERSION.SDK_INT < Build.VERSION_CODES.R;
51+
4652
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
4753

4854
private lateinit var app: HttpToolkitApplication
@@ -308,7 +314,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
308314
Log.i(TAG, if (vpnIntent != null) "got intent" else "no intent")
309315
val vpnNotConfigured = vpnIntent != null
310316

311-
if (whereIsCertTrusted(config) == null) {
317+
if (whereIsCertTrusted(config) == null && PROMPTED_CERT_SETUP_SUPPORTED) {
312318
// The cert isn't trusted, and the VPN may need setup, so there'll be a series of prompts
313319
// here. Explain them beforehand, so users understand what's going on.
314320
withContext(Dispatchers.Main) {
@@ -446,14 +452,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
446452
SCAN_REQUEST -> "scan-request"
447453
else -> requestCode.toString()
448454
})
455+
449456
Log.i(TAG, if (resultCode == RESULT_OK) "ok" else resultCode.toString())
450457

451-
if (resultCode == RESULT_OK) {
458+
val resultOk = resultCode == RESULT_OK ||
459+
(requestCode == INSTALL_CERT_REQUEST && whereIsCertTrusted(currentProxyConfig!!) != null)
460+
461+
if (resultOk) {
452462
if (requestCode == START_VPN_REQUEST && currentProxyConfig != null) {
453463
Log.i(TAG, "Installing cert")
454464
ensureCertificateTrusted(currentProxyConfig!!)
455465
} else if (requestCode == INSTALL_CERT_REQUEST) {
456466
Log.i(TAG, "Starting VPN")
467+
app.trackEvent("Setup", "installed-cert-successfully")
457468
startService(Intent(this, ProxyVpnService::class.java).apply {
458469
action = START_VPN_ACTION
459470
putExtra(PROXY_CONFIG_EXTRA, currentProxyConfig)
@@ -512,20 +523,77 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
512523
app.trackEvent("Setup", "installing-cert")
513524
Log.i(TAG, "Certificate not trusted, prompting to install")
514525

515-
// Install the required cert into the user CA store. Notably, if the cert is already
516-
// installed as a system cert but disabled, this will get triggered, and will enable
517-
// the cert, rather than adding a user cert.
518-
val certInstallIntent = KeyChain.createInstallIntent()
519-
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
520-
certInstallIntent.putExtra(EXTRA_CERTIFICATE, proxyConfig.certificate.encoded)
521-
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
526+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
527+
app.trackEvent("Setup", "installing-cert-manually")
528+
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
529+
// then tell the user how to install it manually:
530+
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
531+
} else {
532+
// Up until Android 11, we can prompt the user to install the CA cert into the user
533+
// CA store. Notably, if the cert is already installed as a system cert but
534+
// disabled, this will get triggered, and will enable the cert, rather than adding
535+
// a normal user cert.
536+
app.trackEvent("Setup", "installing-cert-automatically")
537+
val certInstallIntent = KeyChain.createInstallIntent()
538+
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
539+
certInstallIntent.putExtra(EXTRA_CERTIFICATE, proxyConfig.certificate.encoded)
540+
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
541+
}
522542
} else {
523543
app.trackEvent("Setup", "existing-$existingTrust-cert")
524544
Log.i(TAG, "Certificate already trusted, continuing")
525545
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
526546
}
527547
}
528548

549+
@RequiresApi(Build.VERSION_CODES.Q)
550+
private suspend fun promptToManuallyInstallCert(cert: Certificate) {
551+
// Get ready to save the cert to downloads:
552+
val downloadsUri = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
553+
554+
val contentDetails = ContentValues().apply {
555+
put(MediaStore.Downloads.DISPLAY_NAME, "HTTP Toolkit Certificate.crt")
556+
put(MediaStore.Downloads.MIME_TYPE, "application/x-x509-ca-cert")
557+
put(MediaStore.Downloads.IS_PENDING, 1)
558+
}
559+
560+
val certUri = contentResolver.insert(downloadsUri, contentDetails)
561+
?: throw RuntimeException("Could not get download cert URI")
562+
563+
// Write cert contents to a file:
564+
withContext(Dispatchers.IO) {
565+
contentResolver.openFileDescriptor(certUri, "w", null).use { f ->
566+
ParcelFileDescriptor.AutoCloseOutputStream(f).write(cert.encoded)
567+
}
568+
}
569+
570+
// All done, mark it as such:
571+
contentDetails.clear()
572+
contentDetails.put(MediaStore.Downloads.IS_PENDING, 0)
573+
contentResolver.update(certUri, contentDetails, null, null)
574+
575+
withContext(Dispatchers.Main) {
576+
MaterialAlertDialogBuilder(this@MainActivity)
577+
.setTitle("Manual setup required")
578+
.setIcon(R.drawable.ic_exclamation_triangle)
579+
.setMessage(
580+
"""
581+
Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup.
582+
583+
To allow HTTP Toolkit to intercept HTTPS traffic:
584+
585+
- Open "Encryption & Credentials" in your security settings
586+
- Select "Install a certificate", and then "CA Certificate"
587+
- Select the HTTP Toolkit certificate
588+
""".trimIndent()
589+
)
590+
.setPositiveButton("Open security settings now") { _, _ ->
591+
startActivityForResult(Intent(Settings.ACTION_SECURITY_SETTINGS), INSTALL_CERT_REQUEST)
592+
}
593+
.show()
594+
}
595+
}
596+
529597
private suspend fun promptToUpdate() {
530598
withContext(Dispatchers.Main) {
531599
MaterialAlertDialogBuilder(this@MainActivity)

0 commit comments

Comments
 (0)