I9ELZl6yUKZ*a;@KMWrxgDZcS9fu87)=5eRhg Y^$xLb|ymo zVps$?1V968>4+gd+JaxS?f&!h^d2g8BJgY-y(q1cu!pg?ptjzF0e+oWd!znLLY~x$ zFET`a5A{3btldCHYl|(CfKq6*USp*APaoutTIixEUF)C%*t1VJpXmJD)$kwU#a=Mb zM5BFr@nIj6n<@+$cy=1wW*DAiWEzQ_O4ps;`-}0Bw}XOg d6yx$`Im@6*#QwjGQnIH#89qRaW 6W_5kNy%?9!Xx?84f1j)(n$ z{zE6Z)}?4(AKZi&z%5DR l5 9FdureSd+;rJcx|hQ>FyZZKLM0zm4!=PyU}%ad`i(uA~!y>UN}WiO=Fn*S$T z Os2Uy?hl z`BM08%za^Dwyi~7dYG)$&{Q&h9v)s~|Hw}vPsQ7|CQ2e`bX$7SKtDzScCPT^tjZu= zrS7cq3M&Udd$Z}eH&cLV{=G8vkM-Idh73t65ZIk(@0vwi0otJLRUa<|A>f+}AfG`n zD>U2HJn*STHvSH=-KVTbXjc34n=OBE7yz!}W!z#6-Udxg2ci7KhYOxp?#m&hUc>-r zKFZ_RWiGPq4?0$)ji+<3jjnwfcOm4Sw~z8S=W#NJ=xPICrp}O3vU7B@T*IU+*EMP8 zqro`Yf5*K;^7~k`S#rKl9SjaTO21BO5c&rphiPu-XUC76jANapvu@A>0Ctfu+k 05eh zV70qh!@7P<%y7s9V0pvmTLC8Di7Y&?)sIWc|I}p2`5T|;_wh@Es!j?ost>n4wPtJE zRJYK^Wi1C#LX_xs&>Im4femjvdMN|BHT2x8gpq;wKkaMI1W&^8HWV&)M;^LvizD=t z)ndYpOi3X^bW#%|rSVeU>gWb#5L06Mb+ zw16O4Os(EkK&f1roG-EZw;zu(tT5=$XaXeEd=KUV61s@DgSzf#sIKcsNGwpBL(afD z&*@uB;1U1 Xl_Y5gA8AS4o8yNdigcT@g zMNe-fWoRyfYQP&Pob;PrR@KgNYeOO5O7qkk7v%@O+Z#yY$0~ATJv25h3#314Bh{JM3+wvc4NVDBO>T&cn z=Xnj$oneJbl-@gpr9Gi@4ip?#h7iQpSeA;^+LDO#P-(ZQH-9iW?8lp5&m4FJ3?%>l zxVcZsdS6_^>z$y;Uu%KBIsVGAk+FYN&CuEM@ Z$p#hEv-P&Rv7rt z2Im0U3Bgu52=Btc{K@espq~K_{kJ%anBvIHEQ;0=C4?awX%FB{ZVxw&VYHt_9M)+v z5pZ5(3VGT!^moZz_MchYE)qf_5l#R=2d@*vhurgTbK8SiOdvfRYFHVXJs(Wfkd+$) z$r3g`2Nh3k`GB`kL~GYe*X#+MvT2%n<%hb?w2+;${MOGLIz2MReJuJs0#In$a?f9k z;<_na$v>>uZ8D%Q)5N4 @n*cD1%&7tzzOFv?sRt-rV;8OfmX z>-*dUdzq5u=;viRAJ-Ops()swZ`->eb+`8ZY>1Tr1aRYD!*indw^xOL7}C8PtIobd zCHU$Y6Q0fTfy6w*%c_Mq^!$5PR(}D1(K;Yfqu@k}Xp)_(scMPhbyY!({GwbKXx0G1 zUbL1$dds^j1?UOSvT+yX6jN>)Id`p{X}p0q5Gk#qa}U@Qo_-k`&FIpYu>%xP5G{qq z$TZ8h9~ZQyJAASSDrNKp#p~NXEtjd`#=vWnEZ@j(;caW4yGy9U1NuYXEfi0 gltJAb9sn pknR?te0Ke*f *Zw$9OhElG6x$3lI>WheL?X>@jU zMtE+r EpU7xau??{c !HFJfnm *f&wmR$k;O?wm;c7(d9@oDM0?~`Rz3_`fFK%_^ z;_*X1zaRxv^|O0dKs3K@ADpLQl~ZX(ewx1_fRBqe*Qtx(n%(o?^`MYr6o#0%?J}a& zEDkPj@w(;NB(ylDo*NhJ;fUbNG(fxk*|&>QE2(E*XS-BrO p*~-_cL_j8pGpA z&?{K`E*s-^wCO9@16rM4|9XN?V9l)-{&dl^ZTDv`XM0JLbb-!Mocx=6Z_eO$PY_<3 zb@`ic ba7Uq7A;(3--DFXSyrQ z%=uf>fzCjOi~I_`MqiTS{(1a(Y&Ln=3DXnHl20CzjvPPDg{izAd+>bm(A{A4Vomo? zOtw(7)cwA>O^ordh(L@De1j#z!TUB!Grz3^1tHfOyRb&0@J#meZ-lOT8zFM4YJa%y%I9MJU>7 z^HTKX1uAziD4QlX=Fe{}jkLMk4`0qa$py;W j}G?#+UUkIUh%G~%dq`{fet@JLq%J;LeVVfe*l+26AJ(U literal 0 HcmV?d00001 From b8bead468637066195e1d7c65dde133aa40112b9 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 12 Nov 2024 18:12:16 +0100 Subject: [PATCH 3/5] Don't show QR code scan button on devices without cameras --- .../java/tech/httptoolkit/android/MainActivity.kt | 13 ++++++++++--- app/src/main/res/values/strings.xml | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt index d256819..2cc706a 100644 --- a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt +++ b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt @@ -238,11 +238,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { 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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9372a71..34c384b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,8 +11,8 @@ Connected Oh no! -To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB. - +To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB. +To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there using the ADB or Frida options. to %s on port %d via ADB tunnel From 15de24485620442474ed41889c9e1a91042b2826 Mon Sep 17 00:00:00 2001 From: Tim PerryDate: Tue, 12 Nov 2024 19:16:36 +0100 Subject: [PATCH 4/5] Reorganize cert prompt flow & simplify 'skip' logic to match --- .../tech/httptoolkit/android/MainActivity.kt | 106 +++++++++--------- 1 file changed, 51 insertions(+), 55 deletions(-) diff --git a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt index 2cc706a..6f552de 100644 --- a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt +++ b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt @@ -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 @@ -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) } @@ -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" @@ -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() @@ -656,19 +614,16 @@ 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") @@ -676,6 +631,39 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { } } + 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) { @@ -713,7 +701,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { Html.fromHtml( """ - 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." + }
To allow HTTP Toolkit to intercept HTTPS traffic: @@ -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() } From bb41a934f4231a46ecf0120654cd9201a3610b6b Mon Sep 17 00:00:00 2001 From: Dmitry Kazantsev <0rufim0@gmail.com> Date: Wed, 13 Nov 2024 16:03:15 +0300 Subject: [PATCH 5/5] Fix Android TV icon for old tv boxes --- .../{drawable => drawable-nodpi}/ic_tv_banner.png | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/res/{drawable => drawable-nodpi}/ic_tv_banner.png (100%) diff --git a/app/src/main/res/drawable/ic_tv_banner.png b/app/src/main/res/drawable-nodpi/ic_tv_banner.png similarity index 100% rename from app/src/main/res/drawable/ic_tv_banner.png rename to app/src/main/res/drawable-nodpi/ic_tv_banner.png