1
1
package tech.httptoolkit.android
2
2
3
- import android.app.KeyguardManager
4
3
import android.content.BroadcastReceiver
5
4
import android.content.Context
6
5
import android.content.Intent
7
6
import android.content.IntentFilter
8
7
import android.net.Uri
9
8
import android.net.VpnService
10
- import android.os.Build
11
9
import android.os.Bundle
12
10
import android.security.KeyChain
13
11
import android.security.KeyChain.EXTRA_CERTIFICATE
14
12
import android.security.KeyChain.EXTRA_NAME
15
- import android.util.Base64
16
13
import android.util.Log
17
14
import android.view.View
18
15
import android.widget.Button
@@ -21,23 +18,10 @@ import android.widget.TextView
21
18
import androidx.annotation.StringRes
22
19
import androidx.appcompat.app.AppCompatActivity
23
20
import androidx.localbroadcastmanager.content.LocalBroadcastManager
24
- import com.beust.klaxon.Klaxon
25
21
import com.google.android.material.dialog.MaterialAlertDialogBuilder
26
22
import io.sentry.Sentry
27
23
import 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
39
24
import java.security.cert.X509Certificate
40
- import java.util.concurrent.TimeUnit
41
25
42
26
43
27
const val START_VPN_REQUEST = 123
@@ -52,15 +36,8 @@ enum class MainState {
52
36
FAILED
53
37
}
54
38
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"
64
41
65
42
class MainActivity : AppCompatActivity (), CoroutineScope by MainScope() {
66
43
@@ -163,8 +140,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
163
140
.setIcon(R .drawable.ic_exclamation_triangle)
164
141
.setMessage(
165
142
" 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."
168
145
)
169
146
.setPositiveButton(" Enable" ) { _, _ ->
170
147
Log .i(TAG , " Prompt confirmed" )
@@ -309,7 +286,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
309
286
" trust your HTTP Toolkit's certificate authority. " +
310
287
" \n\n " +
311
288
" Please accept the following prompts to allow this." +
312
- if (! isDeviceSecured())
289
+ if (! isDeviceSecured(applicationContext ))
313
290
" \n\n " +
314
291
" Due to Android security requirements, trusting the certificate will " +
315
292
" require you to set a PIN, password or pattern for this device."
@@ -445,18 +422,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
445
422
446
423
withContext(Dispatchers .IO ) {
447
424
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))
460
426
connectToVpn(config)
461
427
} catch (e: Exception ) {
462
428
Log .e(TAG , e.toString())
@@ -471,119 +437,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
471
437
}
472
438
}
473
439
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
-
567
440
private fun isVpnConfigured (): Boolean {
568
441
return VpnService .prepare(this ) == null
569
442
}
570
443
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
-
587
444
private fun ensureCertificateTrusted (proxyConfig : ProxyConfig ) {
588
445
if (whereIsCertTrusted(proxyConfig) == null ) {
589
446
app.trackEvent(" Setup" , " installing-cert" )
@@ -603,4 +460,4 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
603
460
}
604
461
}
605
462
606
- }
463
+ }
0 commit comments