Skip to content

Commit

Permalink
Reorganize cert prompt flow & simplify 'skip' logic to match
Browse files Browse the repository at this point in the history
  • Loading branch information
pimterry authored and Rufim committed Nov 13, 2024
1 parent 1ddf38b commit 5457188
Showing 1 changed file with 51 additions and 55 deletions.
106 changes: 51 additions & 55 deletions app/src/main/java/tech/httptoolkit/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import java.security.cert.X509Certificate


const val START_VPN_REQUEST = 123
const val START_VPN_REQUEST_NO_CERT = 124
const val INSTALL_CERT_REQUEST = 456
const val SCAN_REQUEST = 789
const val PICK_APPS_REQUEST = 499
Expand Down Expand Up @@ -343,48 +342,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
Log.i(TAG, if (vpnIntent != null) "got intent" else "no intent")
val vpnNotConfigured = vpnIntent != null

if (whereIsCertTrusted(config) == null && PROMPTED_CERT_SETUP_SUPPORTED) {
// The cert isn't trusted, and the VPN may need setup, so there'll be a series of prompts
// here. Explain them beforehand, so users understand what's going on.
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Enable interception")
.setIcon(R.drawable.ic_info_circle)
.setMessage(
"To intercept traffic from this device, you need to " +
(if (vpnNotConfigured) "activate HTTP Toolkit's VPN and " else "") +
"trust your HTTP Toolkit's certificate authority. " +
"\n\n" +
"Please accept the following prompts to allow this." +
if (!isDeviceSecured(applicationContext))
"\n\n" +
"Due to Android security requirements, trusting the certificate will " +
"require you to set a PIN, password or pattern for this device."
else " To trust the certificate, your device PIN will be required."
)
.setPositiveButton("Ok") { _, _ ->
if (vpnNotConfigured) {
startActivityForResult(vpnIntent, START_VPN_REQUEST)
} else {
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
}
}
.setNegativeButton("Continue without certificate") { _, _ ->
if (vpnNotConfigured) {
startActivityForResult(vpnIntent, START_VPN_REQUEST_NO_CERT)
} else {
onActivityResult(START_VPN_REQUEST_NO_CERT, RESULT_OK, null)
}
}
.show()
}
} else if (vpnNotConfigured) {
// In this case the VPN needs setup, but the cert is trusted already, so it's
// a single confirmation. Pretty clear, no need to explain. This happens if the
// VPN/app was removed from the device in the past, or when using injected system certs.
if (vpnNotConfigured) {
// Show the 'Enable the VPN' prompt
startActivityForResult(vpnIntent, START_VPN_REQUEST)
} else {
// VPN is trusted & cert setup already, lets get to it.
// VPN is trusted already, continue
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
}

Expand Down Expand Up @@ -512,7 +474,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
Log.i(TAG, "onActivityResult: " + (
when (requestCode) {
START_VPN_REQUEST -> "start-vpn"
START_VPN_REQUEST_NO_CERT -> "start-vpn-nocrt"
INSTALL_CERT_REQUEST -> "install-cert"
SCAN_REQUEST -> "scan-request"
PICK_APPS_REQUEST -> "pick-apps"
Expand All @@ -529,9 +490,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
if (requestCode == START_VPN_REQUEST && currentProxyConfig != null) {
Log.i(TAG, "Installing cert...")
ensureCertificateTrusted(currentProxyConfig!!)
} else if (requestCode == START_VPN_REQUEST_NO_CERT && currentProxyConfig != null) {
Log.i(TAG, "Ignore cert...")
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
} else if (requestCode == INSTALL_CERT_REQUEST) {
Log.i(TAG ,"Cert installed, checking notification perms...")
ensureNotificationsEnabled()
Expand Down Expand Up @@ -656,26 +614,56 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
if (existingTrust == null) {
Log.i(TAG, "Certificate not trusted, prompting to install")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
// then tell the user how to install it manually:
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
} else {
if (PROMPTED_CERT_SETUP_SUPPORTED) {
// Up until Android 11, we can prompt the user to install the CA cert into the user
// CA store. Notably, if the cert is already installed as a system cert but
// disabled, this will get triggered, and will enable the cert, rather than adding
// a normal user cert.
val certInstallIntent = KeyChain.createInstallIntent()
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
certInstallIntent.putExtra(EXTRA_CERTIFICATE, proxyConfig.certificate.encoded)
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
launch { promptToAutoInstallCert(proxyConfig.certificate) }
} else {
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
// then tell the user how to install it manually:
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
}
} else {
Log.i(TAG, "Certificate already trusted, continuing")
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
}

private suspend fun promptToAutoInstallCert(certificate: Certificate) {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Enable HTTPS interception")
.setIcon(R.drawable.ic_info_circle)
.setMessage(
"To intercept HTTPS traffic from this device, you need to " +
"trust your HTTP Toolkit's certificate authority. " +
"\n\n" +
"Please accept the following prompts to allow this." +
if (!isDeviceSecured(applicationContext))
"\n\n" +
"Due to Android security requirements, trusting the certificate will " +
"require you to set a PIN, password or pattern for this device."
else " To trust the certificate, your device PIN will be required."
)
.setPositiveButton("Install") { _, _ ->
val certInstallIntent = KeyChain.createInstallIntent()
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
certInstallIntent.putExtra(EXTRA_CERTIFICATE, certificate.encoded)
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
}
.setNeutralButton("Skip") { _, _ ->
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
.setNegativeButton("Cancel") { _, _ ->
disconnect()
}
.setCancelable(false)
.show()
}
}

@RequiresApi(Build.VERSION_CODES.Q)
private suspend fun promptToManuallyInstallCert(cert: Certificate, repeatPrompt: Boolean = false) {
if (!repeatPrompt) {
Expand Down Expand Up @@ -713,7 +701,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
Html.fromHtml(
"""
<p>
Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup.
${
if (PROMPTED_CERT_SETUP_SUPPORTED)
"Automatic certificate installation failed, so it must be done manually."
else
"Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup."
}
</p>
<p>
To allow HTTP Toolkit to intercept HTTPS traffic:
Expand All @@ -740,6 +733,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
.setPositiveButton("Open security settings") { _, _ ->
startActivityForResult(Intent(Settings.ACTION_SECURITY_SETTINGS), INSTALL_CERT_REQUEST)
}
.setNeutralButton("Skip") { _, _ ->
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
.setNegativeButton("Cancel") { _, _ ->
disconnect()
}
Expand Down

0 comments on commit 5457188

Please sign in to comment.