Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use with apk installed certificate #31

Merged
merged 5 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />

<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />

<application
android:name=".HttpToolkitApplication"
android:allowBackup="true"
Expand All @@ -28,7 +33,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="m" android:usesCleartextTraffic="true"
android:largeHeap="true">
android:largeHeap="true"
android:banner="@drawable/ic_tv_banner">
<service
android:name=".ProxyVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
Expand All @@ -46,6 +52,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>

<intent-filter
Expand Down
107 changes: 61 additions & 46 deletions app/src/main/java/tech/httptoolkit/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == VPN_STARTED_BROADCAST) {
mainState = MainState.CONNECTED
currentProxyConfig = intent.getParcelableExtra(PROXY_CONFIG_EXTRA)

Check warning on line 74 in app/src/main/java/tech/httptoolkit/android/MainActivity.kt

View workflow job for this annotation

GitHub Actions / Build

'getParcelableExtra(String!): T?' is deprecated. Deprecated in Java
updateUi()
} else if (intent.action == VPN_STOPPED_BROADCAST) {
mainState = MainState.DISCONNECTED
Expand Down Expand Up @@ -237,11 +237,18 @@
when (mainState) {
MainState.DISCONNECTED -> {
statusText.setText(R.string.disconnected_status)
buttonContainer.visibility = View.VISIBLE

detailContainer.addView(detailText(R.string.disconnected_details))
val hasCamera = this.packageManager
.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

buttonContainer.visibility = View.VISIBLE
buttonContainer.addView(primaryButton(R.string.scan_button, ::scanCode))
if (hasCamera) {
detailContainer.addView(detailText(R.string.disconnected_details))
val scanQrButton = primaryButton(R.string.scan_button, ::scanCode)
buttonContainer.addView(scanQrButton)
} else {
detailContainer.addView(detailText(R.string.disconnected_no_camera_details))
}

val lastProxy = app.lastProxy
if (lastProxy != null) {
Expand All @@ -256,7 +263,7 @@
}
MainState.CONNECTED -> {
val proxyConfig = this.currentProxyConfig!!
val totalAppCount = packageManager.getInstalledPackages(PackageManager.GET_META_DATA)

Check warning on line 266 in app/src/main/java/tech/httptoolkit/android/MainActivity.kt

View workflow job for this annotation

GitHub Actions / Build

'getInstalledPackages(Int): (Mutable)List<PackageInfo!>' is deprecated. Deprecated in Java
.map { app -> app.packageName }
.toSet()
.size
Expand Down Expand Up @@ -335,41 +342,11 @@
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)
}
}
.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 @@ -637,26 +614,56 @@
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 @@ -694,7 +701,12 @@
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 @@ -721,6 +733,9 @@
.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
Binary file added app/src/main/res/drawable-nodpi/ic_tv_banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<string name="connected_status">Connected</string>
<string name="failed_status">Oh no!</string>

<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.
</string>
<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.</string>
<string name="disconnected_no_camera_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there using the ADB or Frida options.</string>

<string name="connected_details">to %s on port %d</string>
<string name="connected_tunnel_details">via ADB tunnel</string>
Expand Down
Loading