11package tech.httptoolkit.android
22
3- import android.app.KeyguardManager
43import android.content.BroadcastReceiver
54import android.content.Context
65import android.content.Intent
76import android.content.IntentFilter
87import android.net.Uri
98import android.net.VpnService
10- import android.os.Build
119import android.os.Bundle
1210import android.security.KeyChain
1311import android.security.KeyChain.EXTRA_CERTIFICATE
1412import android.security.KeyChain.EXTRA_NAME
15- import android.util.Base64
1613import android.util.Log
1714import android.view.View
1815import android.widget.Button
@@ -21,23 +18,10 @@ import android.widget.TextView
2118import androidx.annotation.StringRes
2219import androidx.appcompat.app.AppCompatActivity
2320import androidx.localbroadcastmanager.content.LocalBroadcastManager
24- import com.beust.klaxon.Klaxon
2521import com.google.android.material.dialog.MaterialAlertDialogBuilder
2622import io.sentry.Sentry
2723import kotlinx.coroutines.*
28- import okhttp3.OkHttpClient
29- import okhttp3.Request
30- import java.io.ByteArrayInputStream
31- import java.net.ConnectException
32- import java.net.InetSocketAddress
33- import java.net.Proxy
34- import java.nio.charset.StandardCharsets
35- import java.security.KeyStore
36- import java.security.MessageDigest
37- import java.security.cert.CertificateException
38- import java.security.cert.CertificateFactory
3924import java.security.cert.X509Certificate
40- import java.util.concurrent.TimeUnit
4125
4226
4327const val START_VPN_REQUEST = 123
@@ -52,15 +36,8 @@ enum class MainState {
5236 FAILED
5337}
5438
55- private fun getCertificateFingerprint (cert : X509Certificate ): String {
56- val md = MessageDigest .getInstance(" SHA-256" )
57- md.update(cert.publicKey.encoded)
58- val fingerprint = md.digest()
59- return Base64 .encodeToString(fingerprint, Base64 .NO_WRAP )
60- }
61-
62- private val ACTIVATE_INTENT = " tech.httptoolkit.android.ACTIVATE"
63- private val DEACTIVATE_INTENT = " tech.httptoolkit.android.DEACTIVATE"
39+ private const val ACTIVATE_INTENT = " tech.httptoolkit.android.ACTIVATE"
40+ private const val DEACTIVATE_INTENT = " tech.httptoolkit.android.DEACTIVATE"
6441
6542class MainActivity : AppCompatActivity (), CoroutineScope by MainScope() {
6643
@@ -163,8 +140,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
163140 .setIcon(R .drawable.ic_exclamation_triangle)
164141 .setMessage(
165142 " Do you want to share all this device's HTTP traffic with HTTP Toolkit?" +
166- " \n\n " +
167- " Only accept this if you trust the source."
143+ " \n\n " +
144+ " Only accept this if you trust the source."
168145 )
169146 .setPositiveButton(" Enable" ) { _, _ ->
170147 Log .i(TAG , " Prompt confirmed" )
@@ -309,7 +286,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
309286 " trust your HTTP Toolkit's certificate authority. " +
310287 " \n\n " +
311288 " Please accept the following prompts to allow this." +
312- if (! isDeviceSecured())
289+ if (! isDeviceSecured(applicationContext ))
313290 " \n\n " +
314291 " Due to Android security requirements, trusting the certificate will " +
315292 " require you to set a PIN, password or pattern for this device."
@@ -445,18 +422,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
445422
446423 withContext(Dispatchers .IO ) {
447424 try {
448- val dataBase64 = uri.getQueryParameter(" data" )
449-
450- // Data is a JSON string, encoded as base64, to solve escaping & ensure that the
451- // most popular standard barcode apps treat it as a single URL (some get confused by
452- // JSON that contains ip addresses otherwise)
453- val data = String (Base64 .decode(dataBase64, Base64 .URL_SAFE ), StandardCharsets .UTF_8 )
454- Log .d(TAG , " URL data is $data " )
455-
456- val proxyInfo = Klaxon ().parse<ProxyInfo >(data)
457- ? : throw IllegalArgumentException (" Invalid proxy JSON: $data " )
458-
459- val config = getProxyConfig(proxyInfo)
425+ val config = getProxyConfig(parseConnectUri(uri))
460426 connectToVpn(config)
461427 } catch (e: Exception ) {
462428 Log .e(TAG , e.toString())
@@ -471,119 +437,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
471437 }
472438 }
473439
474- private suspend fun getProxyConfig (proxyInfo : ProxyInfo ): ProxyConfig {
475- return withContext(Dispatchers .IO ) {
476- Log .v(TAG , " Validating proxy info $proxyInfo " )
477-
478- val proxyTests = proxyInfo.addresses.map { address ->
479- supervisorScope {
480- async {
481- testProxyAddress(
482- address,
483- proxyInfo.port,
484- proxyInfo.certFingerprint
485- )
486- }
487- }
488- }
489-
490- // Returns with the first working proxy config (cert & address),
491- // or throws if all possible addresses are unreachable/invalid
492- // Once the first test succeeds, we cancel any others
493- val result = proxyTests.awaitFirst()
494- proxyTests.forEach { test ->
495- test.cancel()
496- }
497- return @withContext result
498- }
499- }
500-
501- private suspend fun testProxyAddress (
502- address : String ,
503- port : Int ,
504- expectedFingerprint : String
505- ): ProxyConfig {
506- return withContext(Dispatchers .IO ) {
507- val certFactory = CertificateFactory .getInstance(" X.509" )
508-
509- val httpClient = OkHttpClient .Builder ()
510- .proxy(Proxy (Proxy .Type .HTTP , InetSocketAddress (address, port)))
511- .connectTimeout(2 , TimeUnit .SECONDS )
512- .readTimeout(2 , TimeUnit .SECONDS )
513- .build()
514-
515- val request = Request .Builder ()
516- .url(" http://android.httptoolkit.tech/config" )
517- .build()
518-
519- try {
520- val configString = httpClient.newCall(request).execute().use { response ->
521- if (response.code != 200 ) {
522- throw ConnectException (" Proxy responded with non-200: ${response.code} " )
523- }
524- response.body!! .string()
525- }
526- val config = Klaxon ().parse<ReceivedProxyConfig >(configString)!!
527-
528- val foundCert = certFactory.generateCertificate(
529- ByteArrayInputStream (config.certificate.toByteArray(Charsets .UTF_8 ))
530- ) as X509Certificate
531- val foundCertFingerprint = getCertificateFingerprint(foundCert)
532-
533- if (foundCertFingerprint == expectedFingerprint) {
534- ProxyConfig (
535- address,
536- port,
537- foundCert
538- )
539- } else {
540- throw CertificateException (
541- " Proxy returned mismatched certificate: '${
542- expectedFingerprint
543- } ' != '$foundCertFingerprint ' ($address )"
544- )
545- }
546- } catch (e: Exception ) {
547- Log .i(TAG , " Error testing proxy address $address : $e " )
548- throw e
549- }
550- }
551- }
552-
553- /* *
554- * Does the device have a PIN/pattern/password set? Relevant because if not, the cert
555- * setup will require the user to add one.
556- */
557- private fun isDeviceSecured (): Boolean {
558- val keyguardManager = getSystemService(Context .KEYGUARD_SERVICE ) as KeyguardManager
559- return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
560- keyguardManager.isDeviceSecure
561- } else {
562- // Imperfect but close though: also returns true if the device has a locked SIM card.
563- keyguardManager.isKeyguardSecure
564- }
565- }
566-
567440 private fun isVpnConfigured (): Boolean {
568441 return VpnService .prepare(this ) == null
569442 }
570443
571- // Returns the name of the cert store (if the cert is trusted) or null (if not)
572- private fun whereIsCertTrusted (proxyConfig : ProxyConfig ): String? {
573- val keyStore = KeyStore .getInstance(" AndroidCAStore" )
574- keyStore.load(null , null )
575-
576- val certificateAlias = keyStore.getCertificateAlias(proxyConfig.certificate)
577- Log .i(TAG , " Cert alias $certificateAlias " )
578-
579- return when {
580- certificateAlias == null -> null
581- certificateAlias.startsWith(" system:" ) -> " system"
582- certificateAlias.startsWith(" user:" ) -> " user"
583- else -> " unknown-store"
584- }
585- }
586-
587444 private fun ensureCertificateTrusted (proxyConfig : ProxyConfig ) {
588445 if (whereIsCertTrusted(proxyConfig) == null ) {
589446 app.trackEvent(" Setup" , " installing-cert" )
@@ -603,4 +460,4 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
603460 }
604461 }
605462
606- }
463+ }
0 commit comments