├── .gitignore ├── README.md ├── app ├── .gitignore ├── .idea │ ├── .gitignore │ └── codeStyles │ │ ├── Project.xml │ │ └── codeStyleConfig.xml ├── app │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── kotlin │ │ └── com │ │ │ └── example │ │ │ └── insecuretls │ │ │ ├── WebviewActivity.kt │ │ │ └── ui │ │ │ └── main │ │ │ ├── SectionsPagerAdapter.kt │ │ │ └── WebViewFragment.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_webview.xml │ │ └── fragment_webview.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── raw │ │ └── mitmtest_ca.crt │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ └── network_security_config.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle ├── backend ├── nginx-backend.conf ├── nginx-certs │ ├── mitmtest.com.crt │ └── mitmtest.com.key └── server-files │ └── index.html ├── docker-compose.yaml ├── eve ├── Dockerfile └── eve_files │ ├── fake-cert.pem │ ├── proxy.py │ └── start.sh ├── run.sh └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | android-emulator-container-scripts/ 2 | __pycache__/ 3 | .idea/ 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Containerized Demo for Insecure TLS Certificate Checking in Android 2 | 3 | ## Overview 4 | 5 | This repository contains the files you need to run the demos for our blog post series 6 | on TLS certificate checking in Android apps. The 7 | [first post](https://www.guardsquare.com/blog/insecure-tls-certificate-checking-in-android-apps) 8 | covers common implementation errors and the 9 | [second one](https://guardsquare.com/blog/how-to-securely-implement-tls-certificate-checking-in-android-apps) 10 | then explains how you can securely configure TLS connections even in cases when you have 11 | to deviate from the default behavior. There are two parts to the repo: 12 | 13 | 1. **Example app:** In [app/](app/) you will find the full AndroidStudio project for the example app that showcases 14 | different TLS checking implementations. 15 | 2. **Docker setup:** By running [setup.sh](setup.sh) you prepare a Docker environment consisting of several containers: 16 | An Android emulator is spawned and a web frontend to interact with it is made available on https://localhost 17 | (Note that this frontend uses a self-signed certificate). Additionally, an example web server container is 18 | created, which will be used as the backend for the demo scenarios. The last part of the setup is an attacker 19 | container, through which you will be able to interactively intercept web traffic between backend server and 20 | Android emulator. 21 | 22 | ## Scenario Overview 23 | 24 | The backend serves a simple HTML website over HTTPs. This mimicks the situation where sensitive data is provided 25 | over a secure connection. The catch is that the certificate it uses (see [backend/nginx-certs/](backend/nginx-certs)) 26 | has not been issued by a globally trusted CA but rather by a custom one. 27 | 28 | The Android app in [app/](app/) fetches the data provided by this server and displays it to the user. In order to 29 | make Android accept the custom certificate, the default certificate checking mechanism needs to be modified. 30 | To showcase different insecure ways of doing so, the app consists of several tabs, where each 31 | fetches the data using a different workaround commonly found online. You can check out the corresponding 32 | source code in [WebViewFragment.kt](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt). 33 | 34 | In the first blog post we cover three different types of implementation errors: 35 | 36 | 1. **[WebView ignores all SSL errors](https://www.guardsquare.com/blog/insecure-tls-certificate-checking-in-android-apps#webview):** 37 | See [setupInsecureWebView()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L58) 38 | 2. **[Malfunctioning X509TrustManager Implementations](https://www.guardsquare.com/blog/insecure-tls-certificate-checking-in-android-apps#Malfunctioning):** 39 | See [setupInsecureTrustManager()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L108) 40 | 3. **[Disabled Host Name Checks](https://www.guardsquare.com/blog/insecure-tls-certificate-checking-in-android-apps#host_name):** 41 | See [setupInsecureHostnameVerifier()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L82) 42 | 43 | The second blog post explains how to configure non-standard certificate checking behaviors in a secure way: 44 | 45 | 1. **[Allowing Custom Certificate Authorities](https://www.guardsquare.com/blog/how-to-securely-implement-tls-certificate-checking-in-android-apps#Allowing_Custom_Certificate_Authorities):** 46 | 1. **SDK 24 And Newer:** See [setupNetworkSecurityConfig()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L165) 47 | and [network_security_config.xml](app/app/src/main/res/xml/network_security_config.xml) 48 | 2. **Older Versions:** See [setupCustomCaLegacy()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L211) 49 | and [setupCustomCaLegacyWebview()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L260) 50 | 2. **[Certificate Pinning](https://www.guardsquare.com/blog/how-to-securely-implement-tls-certificate-checking-in-android-apps#Certificate_Pinning):** 51 | 1. **SDK 24 And Newer:** See [setupNetworkSecurityConfig()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L165) 52 | and [network_security_config.xml](app/app/src/main/res/xml/network_security_config.xml) 53 | 2. **Older Versions:** See [setupPinningLegacy()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L323) 54 | and [setupPinningLegacyWebview()](app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt#L405) 55 | 56 | This app will be installed to a containerized Android emulator that lives in the same virtual network as 57 | the backend server and the attacker. Setting this network up is explained in the next section. 58 | 59 | ## Prerequisites 60 | 61 | In order to launch the demo environment, you will need to have [docker-compose](https://docs.docker.com/compose/install/) 62 | installed, as well as Python3, NodeJS and npm. Also make sure to have the Android SDK installed (SDK platform version 31). 63 | The `ANDROID_SDK_ROOT` environment variable needs to point to its installation directory, usually `~/Android/Sdk`. 64 | All other necessary dependencies will be downloaded automatically. 65 | 66 | ## Docker Setup 67 | 68 | Running [setup.sh](setup.sh) will get the necessary files to set up the Docker containers, which may take a while, 69 | depending on your system performance and Internet speed. Afterwards you can launch the containers with [run.sh](run.sh). 70 | This script takes care of several things: 71 | 72 | 1. The Android emulator will be booted and a web interface to interact with it is made available on 73 | https://localhost (Note that the website uses a self-signed certificate). Login with username `user` and password 74 | `pass`. Then you should see the emulator screen, with which you can interact using your mouse. 75 | 2. In the meantime, the example app is compiled and once the emulator is fully booted up it is installed 76 | and launched automatically. 77 | 3. Once the app is running, a bash shell is opened on the attacker container so that you can interactively 78 | experiment with the man-in-the-middle setup. As a quick start, you can simply execute the [start.sh](eve/eve_files/start.sh) 79 | script that you will find in the current working directory where the shell was spawned (`/eve_files` on the container). 80 | 81 | This script sets up the attacker proxy using the [mitmproxy](https://mitmproxy.org/) tool without needing 82 | any user input. You can then observe intercepted traffic in the console that will show up. 83 | To exit the console and stop the attack, simply press Ctrl+C and confirm. Should you want to deviate 84 | from the default attacker script, feel free to inspect [start.sh](eve/eve_files/start.sh) and the associated 85 | [proxy.py](eve/eve_files/proxy.py) file. 86 | 87 | If you would like to experiment with the certificate pinning implementations, 88 | `start.sh` allows you to pass `--custom-ca`, which will instruct `mitmproxy` to use a certificate 89 | that was signed by the same custom CA that the example server uses. This mimics the situation that the 90 | attacker is indeed able to get a valid certificate for your domain, which would be trusted under normal circumstances. 91 | The additional certificate pinning step however is able to successfully detect the attack and refuse the connection. 92 | 5. After you are done exploring the demos, simply exit the attacker shell as usual (Ctrl+D or typing `exit`). 93 | This will automatically shut down the containers in a clean way. 94 | 95 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/androidstudio 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=androidstudio 4 | 5 | ### AndroidStudio ### 6 | # Covers files to be ignored for android development using Android Studio. 7 | 8 | # Built application files 9 | *.apk 10 | *.ap_ 11 | *.aab 12 | 13 | # Files for the ART/Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | out/ 23 | 24 | # Gradle files 25 | .gradle 26 | .gradle/ 27 | build/ 28 | 29 | # Signing files 30 | .signing/ 31 | 32 | # Local configuration file (sdk path, etc) 33 | local.properties 34 | 35 | # Proguard folder generated by Eclipse 36 | proguard/ 37 | 38 | # Log Files 39 | *.log 40 | 41 | # Android Studio 42 | /*/build/ 43 | /*/local.properties 44 | /*/out 45 | /*/*/build 46 | /*/*/production 47 | captures/ 48 | .navigation/ 49 | *.ipr 50 | *~ 51 | *.swp 52 | 53 | # Keystore files 54 | *.jks 55 | *.keystore 56 | 57 | # Google Services (e.g. APIs or Firebase) 58 | # google-services.json 59 | 60 | # Android Patch 61 | gen-external-apklibs 62 | 63 | # External native build folder generated in Android Studio 2.2 and later 64 | .externalNativeBuild 65 | 66 | # NDK 67 | obj/ 68 | 69 | # IntelliJ IDEA 70 | *.iml 71 | *.iws 72 | /out/ 73 | 74 | # User-specific configurations 75 | .idea/caches/ 76 | .idea/libraries/ 77 | .idea/shelf/ 78 | .idea/workspace.xml 79 | .idea/tasks.xml 80 | .idea/.name 81 | .idea/compiler.xml 82 | .idea/copyright/profiles_settings.xml 83 | .idea/encodings.xml 84 | .idea/misc.xml 85 | .idea/modules.xml 86 | .idea/scopes/scope_settings.xml 87 | .idea/dictionaries 88 | .idea/vcs.xml 89 | .idea/jsLibraryMappings.xml 90 | .idea/datasources.xml 91 | .idea/dataSources.ids 92 | .idea/sqlDataSources.xml 93 | .idea/dynamic.xml 94 | .idea/uiDesigner.xml 95 | .idea/assetWizardSettings.xml 96 | .idea/gradle.xml 97 | .idea/jarRepositories.xml 98 | .idea/navEditor.xml 99 | 100 | # OS-specific files 101 | .DS_Store 102 | .DS_Store? 103 | ._* 104 | .Spotlight-V100 105 | .Trashes 106 | ehthumbs.db 107 | Thumbs.db 108 | 109 | # Legacy Eclipse project files 110 | .classpath 111 | .project 112 | .cproject 113 | .settings/ 114 | 115 | # Mobile Tools for Java (J2ME) 116 | .mtj.tmp/ 117 | 118 | # Package Files # 119 | *.war 120 | *.ear 121 | 122 | # virtual machine crash logs (Reference: http://www.java.com/en/download/help/error_hotspot.xml) 123 | hs_err_pid* 124 | 125 | ## Plugin-specific files: 126 | 127 | # mpeltonen/sbt-idea plugin 128 | .idea_modules/ 129 | 130 | # JIRA plugin 131 | atlassian-ide-plugin.xml 132 | 133 | # Mongo Explorer plugin 134 | .idea/mongoSettings.xml 135 | 136 | # Crashlytics plugin (for Android Studio and IntelliJ) 137 | com_crashlytics_export_strings.xml 138 | crashlytics.properties 139 | crashlytics-build.properties 140 | fabric.properties 141 | 142 | ### AndroidStudio Patch ### 143 | 144 | !/gradle/wrapper/gradle-wrapper.jar 145 | 146 | # End of https://www.toptal.com/developers/gitignore/api/androidstudio -------------------------------------------------------------------------------- /app/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /app/.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | -------------------------------------------------------------------------------- /app/.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /app/app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | compileSdk 31 8 | 9 | defaultConfig { 10 | applicationId "com.example.insecuretls" 11 | minSdk 21 12 | targetSdk 31 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | compileOptions { 26 | sourceCompatibility JavaVersion.VERSION_1_8 27 | targetCompatibility JavaVersion.VERSION_1_8 28 | } 29 | kotlinOptions { 30 | jvmTarget = '1.8' 31 | } 32 | 33 | buildFeatures { 34 | viewBinding true 35 | } 36 | } 37 | 38 | dependencies { 39 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' 40 | implementation 'androidx.core:core-ktx:1.7.0' 41 | implementation 'androidx.appcompat:appcompat:1.4.0' 42 | implementation 'com.google.android.material:material:1.4.0' 43 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 44 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.0' 45 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0' 46 | implementation 'com.squareup.okhttp3:okhttp:4.9.2' 47 | } -------------------------------------------------------------------------------- /app/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/app/src/main/kotlin/com/example/insecuretls/WebviewActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.insecuretls 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.viewpager.widget.ViewPager 6 | import com.example.insecuretls.databinding.ActivityWebviewBinding 7 | import com.example.insecuretls.ui.main.SectionsPagerAdapter 8 | import com.google.android.material.tabs.TabLayout 9 | 10 | class WebviewActivity : AppCompatActivity() { 11 | 12 | private lateinit var binding: ActivityWebviewBinding 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | binding = ActivityWebviewBinding.inflate(layoutInflater) 18 | setContentView(binding.root) 19 | 20 | val sectionsPagerAdapter = SectionsPagerAdapter(this, supportFragmentManager) 21 | val viewPager: ViewPager = binding.viewPager 22 | viewPager.adapter = sectionsPagerAdapter 23 | val tabs: TabLayout = binding.tabs 24 | tabs.setupWithViewPager(viewPager) 25 | } 26 | } -------------------------------------------------------------------------------- /app/app/src/main/kotlin/com/example/insecuretls/ui/main/SectionsPagerAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.example.insecuretls.ui.main 2 | 3 | import android.content.Context 4 | import androidx.fragment.app.Fragment 5 | import androidx.fragment.app.FragmentManager 6 | import androidx.fragment.app.FragmentPagerAdapter 7 | 8 | enum class Implementation(val title: String) { 9 | WEBVIEW_IGNORING_TLS_ERRORS("Insecure WebViewClient"), 10 | TLS_CERTIFICATE_CHECK_DISABLED("Insecure HostnameVerifier"), 11 | MALFUNCTIONING_X509_TRUST_MANAGER("Insecure X509TrustManager"), 12 | NETWORK_SECURITY_CONFIG("Network Security Config"), 13 | CUSTOM_CA_LEGACY("Custom CA: Trust Manager"), 14 | CUSTOM_CA_LEGACY_WEBVIEW("Custom CA: WebView"), 15 | PINNING_LEGACY("Pinning: OkHttp"), 16 | PINNING_LEGACY_WEBVIEW("Pinning: WebViews") 17 | } 18 | 19 | /** 20 | * A [FragmentPagerAdapter] that returns a fragment corresponding to 21 | * one of the sections/tabs/pages. 22 | */ 23 | class SectionsPagerAdapter(private val context: Context, fm: FragmentManager) : 24 | FragmentPagerAdapter(fm) { 25 | 26 | override fun getItem(position: Int): Fragment { 27 | // getItem is called to instantiate the fragment for the given page. 28 | // Return a PlaceholderFragment (defined as a static inner class below). 29 | return WebViewFragment.newInstance(position) 30 | } 31 | 32 | override fun getPageTitle(position: Int): CharSequence { 33 | return Implementation.values()[position].title 34 | } 35 | 36 | override fun getCount(): Int { 37 | return Implementation.values().size 38 | } 39 | } -------------------------------------------------------------------------------- /app/app/src/main/kotlin/com/example/insecuretls/ui/main/WebViewFragment.kt: -------------------------------------------------------------------------------- 1 | package com.example.insecuretls.ui.main 2 | 3 | import android.net.http.SslError 4 | import android.os.Build.VERSION.SDK_INT 5 | import android.os.Bundle 6 | import android.os.Handler 7 | import android.os.Looper 8 | import android.util.Base64 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.ViewGroup 12 | import android.webkit.* 13 | import androidx.appcompat.app.AlertDialog 14 | import androidx.fragment.app.Fragment 15 | import com.example.insecuretls.R 16 | import com.example.insecuretls.databinding.FragmentWebviewBinding 17 | import kotlinx.coroutines.Dispatchers.Main 18 | import kotlinx.coroutines.GlobalScope 19 | import kotlinx.coroutines.async 20 | import kotlinx.coroutines.withContext 21 | import okhttp3.CertificatePinner 22 | import okhttp3.OkHttpClient 23 | import okhttp3.Request 24 | import java.io.BufferedInputStream 25 | import java.net.URL 26 | import java.security.KeyStore 27 | import java.security.SecureRandom 28 | import java.security.cert.CertificateFactory 29 | import java.security.cert.X509Certificate 30 | import javax.net.ssl.* 31 | 32 | class WebViewFragment : Fragment() { 33 | 34 | private lateinit var implementation: Implementation 35 | private var _binding: FragmentWebviewBinding? = null 36 | 37 | private val binding get() = _binding!! 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | implementation = Implementation.values()[arguments?.getInt(ARG_SECTION_NUMBER) ?: 0] 42 | } 43 | 44 | override fun onCreateView( 45 | inflater: LayoutInflater, container: ViewGroup?, 46 | savedInstanceState: Bundle? 47 | ): View { 48 | 49 | _binding = FragmentWebviewBinding.inflate(inflater, container, false) 50 | 51 | when (implementation) { 52 | Implementation.WEBVIEW_IGNORING_TLS_ERRORS -> setupInsecureWebView() 53 | Implementation.TLS_CERTIFICATE_CHECK_DISABLED -> setupInsecureHostnameVerifier() 54 | Implementation.MALFUNCTIONING_X509_TRUST_MANAGER -> setupInsecureTrustManager() 55 | Implementation.NETWORK_SECURITY_CONFIG -> setupNetworkSecurityConfig() 56 | Implementation.CUSTOM_CA_LEGACY -> setupCustomCaLegacy() 57 | Implementation.CUSTOM_CA_LEGACY_WEBVIEW -> setupCustomCaLegacyWebview() 58 | Implementation.PINNING_LEGACY -> setupPinningLegacy() 59 | Implementation.PINNING_LEGACY_WEBVIEW -> setupPinningLegacyWebview() 60 | } 61 | 62 | return binding.root 63 | } 64 | 65 | /** 66 | * Set up the WebView to ignore all SSL errors. 67 | */ 68 | private fun setupInsecureWebView() { 69 | binding.webview.webViewClient = object : WebViewClient() { 70 | override fun onReceivedSslError( 71 | view: WebView?, 72 | handler: SslErrorHandler?, 73 | error: SslError? 74 | ) { 75 | handler?.proceed() 76 | } 77 | } 78 | 79 | binding.fab.setOnClickListener { 80 | if (binding.webview.url == null) { 81 | binding.webview.loadUrl("https://www.mitmtest.com") 82 | } else { 83 | binding.webview.clearCache(true) 84 | binding.webview.reload() 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * Disable the host name checking for SSL certificates. 91 | */ 92 | private fun setupInsecureHostnameVerifier() { 93 | binding.fab.setOnClickListener { 94 | val url = URL("https://wrong.host.badssl.com") 95 | val connection = url.openConnection() as HttpsURLConnection 96 | connection.hostnameVerifier = HostnameVerifier { hostname, session -> true } 97 | 98 | GlobalScope.async { 99 | val content = connection.inputStream.bufferedReader().use { it.readText() } 100 | 101 | withContext(Main) { 102 | binding.webview.loadData( 103 | Base64.encodeToString( 104 | content.toByteArray(), 105 | Base64.NO_PADDING 106 | ), 107 | "text/html", 108 | "base64" 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Create a malfunctioning X509TrustManager implementation. 117 | */ 118 | private fun setupInsecureTrustManager() { 119 | binding.fab.setOnClickListener { 120 | val insecureTrustManager = object : X509TrustManager { 121 | override fun checkClientTrusted( 122 | chain: Array?, 123 | authType: String? 124 | ) { 125 | // do nothing 126 | } 127 | 128 | override fun checkServerTrusted( 129 | chain: Array?, 130 | authType: String? 131 | ) { 132 | // do nothing 133 | } 134 | 135 | override fun getAcceptedIssuers() = null 136 | } 137 | val context = SSLContext.getInstance(TLS_VERSION) 138 | context.init(null, arrayOf(insecureTrustManager), SecureRandom()) 139 | 140 | val url = URL("https://www.mitmtest.com") 141 | val connection = url.openConnection() as HttpsURLConnection 142 | connection.sslSocketFactory = context.socketFactory 143 | 144 | GlobalScope.async { 145 | val content = connection.inputStream.bufferedReader().use { it.readText() } 146 | 147 | withContext(Main) { 148 | binding.webview.loadData( 149 | Base64.encodeToString( 150 | content.toByteArray(), 151 | Base64.NO_PADDING 152 | ), 153 | "text/html", 154 | "base64" 155 | ) 156 | } 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Setup the WebView without any custom trust manager, to showcase how the network security 163 | * configuration xml works without code changes. 164 | */ 165 | private fun setupNetworkSecurityConfig() { 166 | binding.webview.webViewClient = object : WebViewClient() { 167 | override fun onReceivedSslError( 168 | view: WebView?, 169 | handler: SslErrorHandler?, 170 | error: SslError? 171 | ) { 172 | val cause = when (error?.primaryError) { 173 | SslError.SSL_NOTYETVALID -> "The certificate is only valid after ${error.certificate.validNotBeforeDate}" 174 | SslError.SSL_EXPIRED -> "The certificate has expired on ${error.certificate.validNotAfterDate}" 175 | SslError.SSL_IDMISMATCH -> "Hostname mismatch (url ${view?.url} but certificate is for ${error.certificate.issuedTo.cName})" 176 | SslError.SSL_UNTRUSTED -> "The certificate ${error.certificate} is not trusted" 177 | SslError.SSL_DATE_INVALID -> "Date is invalid" 178 | else -> "An unknown error occurred" 179 | } 180 | if (context != null) { 181 | val builder = AlertDialog.Builder(context!!) 182 | builder.apply { 183 | setTitle("SSL Error") 184 | setMessage( 185 | "Error validating server certificate: $cause\n" + 186 | "Attackers might want to steal your data." 187 | ) 188 | setNegativeButton("Abort") { _, _ -> handler?.cancel() } 189 | } 190 | builder.create().show() 191 | } else { 192 | handler?.cancel() 193 | } 194 | } 195 | } 196 | 197 | binding.fab.setOnClickListener { 198 | if (binding.webview.url == null) { 199 | binding.webview.loadUrl("https://www.mitmtest.com") 200 | } else { 201 | binding.webview.clearCache(true) 202 | binding.webview.reload() 203 | } 204 | } 205 | } 206 | 207 | /** 208 | * Create a trust manager that accepts a custom CA certificate in addition to all 209 | * certificates in the system trust store. 210 | */ 211 | private fun setupCustomCaLegacy() { 212 | binding.fab.setOnClickListener { 213 | val tmf = initTrustManagerFactory() 214 | val sslContext = SSLContext.getInstance(TLS_VERSION) 215 | sslContext.init(null, tmf.trustManagers, null) 216 | 217 | val defaultSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory() 218 | HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) 219 | 220 | val url = URL("https://www.mitmtest.com") 221 | val connection = url.openConnection() as HttpsURLConnection 222 | 223 | GlobalScope.async { 224 | var content: String 225 | try { 226 | content = connection.inputStream.bufferedReader().use { it.readText() } 227 | } catch (e: SSLException) { 228 | content = "" 229 | if (context != null) { 230 | withContext(Main) { 231 | val builder = AlertDialog.Builder(context!!) 232 | builder.apply { 233 | setTitle("SSL Exception") 234 | setMessage(e.message) 235 | setNegativeButton(R.string.abort) { _, _ -> } 236 | } 237 | builder.create().show() 238 | } 239 | } 240 | } 241 | 242 | withContext(Main) { 243 | binding.webview.loadData( 244 | Base64.encodeToString( 245 | content.toByteArray(), 246 | Base64.NO_PADDING 247 | ), 248 | "text/html", 249 | "base64" 250 | ) 251 | } 252 | } 253 | HttpsURLConnection.setDefaultSSLSocketFactory(defaultSocketFactory) 254 | } 255 | } 256 | 257 | /** 258 | * Create a trust manager that is used to accept a custom CA certificate in a WebView. 259 | */ 260 | private fun setupCustomCaLegacyWebview() { 261 | val tmf = initTrustManagerFactory() 262 | 263 | binding.webview.webViewClient = object : WebViewClient() { 264 | override fun onReceivedSslError( 265 | view: WebView?, 266 | handler: SslErrorHandler?, 267 | error: SslError? 268 | ) { 269 | val cause = when (error?.primaryError) { 270 | SslError.SSL_NOTYETVALID -> "The certificate is only valid after ${error.certificate.validNotBeforeDate}" 271 | SslError.SSL_EXPIRED -> "The certificate has expired on ${error.certificate.validNotAfterDate}" 272 | SslError.SSL_IDMISMATCH -> "Hostname mismatch (url ${view?.url} but certificate is for ${error.certificate.issuedTo.cName})" 273 | SslError.SSL_UNTRUSTED -> { 274 | try { 275 | val certField = 276 | error.certificate.javaClass.getDeclaredField("mX509Certificate") 277 | certField.isAccessible = true 278 | val cert = certField.get(error.certificate) as X509Certificate 279 | tmf.trustManagers.forEach { 280 | (it as X509TrustManager).checkServerTrusted( 281 | arrayOf(cert), "generic" 282 | ) 283 | } 284 | handler?.proceed() 285 | return 286 | } catch (e: Exception) { 287 | "The certificate ${error.certificate} is not trusted" 288 | } 289 | } 290 | SslError.SSL_DATE_INVALID -> "Date is invalid" 291 | else -> "An unknown error occurred" 292 | } 293 | if (context != null) { 294 | val builder = AlertDialog.Builder(context!!) 295 | builder.apply { 296 | setTitle("SSL Error") 297 | setMessage( 298 | "Error validating server certificate: $cause\n" + 299 | "Attackers might want to steal your data." 300 | ) 301 | setNegativeButton(R.string.abort) { _, _ -> handler?.cancel() } 302 | } 303 | builder.create().show() 304 | } else { 305 | handler?.cancel() 306 | } 307 | } 308 | } 309 | 310 | binding.fab.setOnClickListener { 311 | if (binding.webview.url == null) { 312 | binding.webview.loadUrl("https://www.mitmtest.com") 313 | } else { 314 | binding.webview.clearCache(true) 315 | binding.webview.reload() 316 | } 317 | } 318 | } 319 | 320 | /** 321 | * Enforce certificate pinning for the server certificate. 322 | */ 323 | private fun setupPinningLegacy() { 324 | binding.fab.setOnClickListener { 325 | val tmf = initTrustManagerFactory() 326 | val sslContext = SSLContext.getInstance(TLS_VERSION) 327 | sslContext.init(null, tmf.trustManagers, null) 328 | 329 | val pinner = CertificatePinner.Builder() 330 | .add("*.mitmtest.com", "sha256/pcG7tltpGuaJrssJiqr5bmYc4iypr3QE65su1XuDcK8=") 331 | .build() 332 | val httpClient = OkHttpClient.Builder() 333 | // Custom socket factory is only needed because the pinned certificate is from a custom CA 334 | .sslSocketFactory(sslContext.socketFactory, tmf.trustManagers[0] as X509TrustManager) 335 | .certificatePinner(pinner) 336 | .build() 337 | val url = URL("https://www.mitmtest.com") 338 | val request = Request.Builder().url(url).build() 339 | 340 | GlobalScope.async { 341 | var content = "" 342 | try { 343 | val response = httpClient.newCall(request).execute() 344 | if (response.isSuccessful) { 345 | content = response.body?.string() ?: "" 346 | } 347 | } catch (e: SSLException) { 348 | if (context != null) { 349 | withContext(Main) { 350 | val builder = AlertDialog.Builder(context!!) 351 | builder.apply { 352 | setTitle("SSL Exception") 353 | setMessage(e.message) 354 | setNegativeButton(R.string.abort) { _, _ -> } 355 | } 356 | builder.create().show() 357 | } 358 | } 359 | } 360 | 361 | withContext(Main) { 362 | binding.webview.loadData( 363 | Base64.encodeToString( 364 | content.toByteArray(), 365 | Base64.NO_PADDING 366 | ), 367 | "text/html", 368 | "base64" 369 | ) 370 | } 371 | } 372 | } 373 | } 374 | 375 | object ContentTypeParser { 376 | private const val PARTS_DELIMITER = ";" 377 | private const val VALUE_DELIMITER = "=" 378 | private const val UTF8 = "UTF-8" 379 | 380 | private const val CHARSET = "charset" 381 | 382 | fun getMimeType(contentType: String): String { 383 | if (contentType.contains(PARTS_DELIMITER)) { 384 | val contentTypeParts = contentType.split(PARTS_DELIMITER.toRegex()) 385 | return contentTypeParts[0].trim() 386 | } 387 | return contentType 388 | } 389 | 390 | fun getCharset(contentType: String): String { 391 | if (contentType.contains(PARTS_DELIMITER)) { 392 | val contentTypeParts = contentType.split(PARTS_DELIMITER.toRegex()) 393 | val charsetParts = contentTypeParts[1].split(VALUE_DELIMITER.toRegex()) 394 | if (charsetParts[0].trim().startsWith(CHARSET)) { 395 | return charsetParts[1].trim().toUpperCase() 396 | } 397 | } 398 | return UTF8 399 | } 400 | } 401 | 402 | /** 403 | * Add certificate pinning to a WebView. 404 | */ 405 | private fun setupPinningLegacyWebview() { 406 | val tmf = initTrustManagerFactory() 407 | val sslContext = SSLContext.getInstance(TLS_VERSION) 408 | sslContext.init(null, tmf.trustManagers, null) 409 | 410 | val pinner = CertificatePinner.Builder() 411 | .add("*.mitmtest.com", "sha256/pcG7tltpGuaJrssJiqr5bmYc4iypr3QE65su1XuDcK8=") 412 | .build() 413 | val httpClient = OkHttpClient.Builder() 414 | // Custom socket factory is only needed because the pinned certificate is from a custom CA 415 | .sslSocketFactory(sslContext.socketFactory, tmf.trustManagers[0] as X509TrustManager) 416 | .certificatePinner(pinner) 417 | .build() 418 | 419 | binding.webview.webViewClient = object : WebViewClient() { 420 | override fun shouldInterceptRequest( 421 | view: WebView, 422 | interceptedRequest: WebResourceRequest 423 | ): WebResourceResponse { 424 | try { 425 | val url = URL(interceptedRequest.url.toString()) 426 | val request = Request.Builder().url(url).build() 427 | val response = httpClient.newCall(request).execute() 428 | 429 | val contentType = response.header("Content-Type") 430 | 431 | if (contentType != null) { 432 | 433 | val inputStream = response.body?.byteStream() 434 | val mimeType = ContentTypeParser.getMimeType(contentType) 435 | val charset = ContentTypeParser.getCharset(contentType) 436 | 437 | return WebResourceResponse(mimeType, charset, inputStream) 438 | } 439 | } catch (e: SSLPeerUnverifiedException) { 440 | Handler(Looper.getMainLooper()).post { 441 | val builder = AlertDialog.Builder(context!!) 442 | builder.apply { 443 | setTitle("Certificate Error") 444 | setMessage(e.message) 445 | setNegativeButton(R.string.abort) { _, _ -> } 446 | } 447 | builder.create().show() 448 | } 449 | } catch (e: Exception) { 450 | Handler(Looper.getMainLooper()).post { 451 | val builder = AlertDialog.Builder(context!!) 452 | builder.apply { 453 | setTitle("Connection Error") 454 | setMessage(e.message ?: "Cause unknown") 455 | setNegativeButton(R.string.abort) { _, _ -> } 456 | } 457 | builder.create().show() 458 | } 459 | } 460 | 461 | return WebResourceResponse(null, null, null) 462 | } 463 | } 464 | 465 | binding.fab.setOnClickListener { 466 | if (binding.webview.url == null) { 467 | binding.webview.loadUrl("https://www.mitmtest.com") 468 | } else { 469 | binding.webview.clearCache(true) 470 | binding.webview.reload() 471 | } 472 | } 473 | } 474 | 475 | private fun initTrustManagerFactory(): TrustManagerFactory { 476 | // Parse CA certificate from the res/raw/my_ca.crt resource 477 | val cf = CertificateFactory.getInstance("X.509") 478 | val caInput = BufferedInputStream(resources.openRawResource(R.raw.mitmtest_ca)) 479 | val ca = caInput.use { 480 | cf.generateCertificate(it) 481 | } 482 | 483 | // Create key store and insert the custom certificate 484 | val keyStoreType = KeyStore.getDefaultType() 485 | val keyStore = KeyStore.getInstance(keyStoreType) 486 | keyStore.load(null, null) 487 | keyStore.setCertificateEntry("custom_ca", ca) 488 | 489 | // Add the default well known certificates as well 490 | val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm() 491 | val defaultTmf = TrustManagerFactory.getInstance(tmfAlgorithm) 492 | defaultTmf.init(null as KeyStore?) 493 | defaultTmf.trustManagers.filterIsInstance() 494 | .flatMap { it.acceptedIssuers.toList() } 495 | .forEach { keyStore.setCertificateEntry(it.subjectDN.name, it) } 496 | 497 | // Create a new trust manager that uses this custom key store 498 | val tmf = TrustManagerFactory.getInstance(tmfAlgorithm) 499 | tmf.init(keyStore) 500 | return tmf 501 | } 502 | 503 | companion object { 504 | /** 505 | * The fragment argument representing the section number for this 506 | * fragment. 507 | */ 508 | private const val ARG_SECTION_NUMBER = "section_number" 509 | 510 | /** 511 | * The currently supported TLS version (SDK 29 and above: 1.3, everything below only 512 | * supports 1.2). 513 | */ 514 | private val TLS_VERSION = if (SDK_INT > 28) "TLSv1.3" else "TLSv1.2" 515 | 516 | /** 517 | * Returns a new instance of this fragment for the given section 518 | * number. 519 | */ 520 | @JvmStatic 521 | fun newInstance(sectionNumber: Int): WebViewFragment { 522 | return WebViewFragment().apply { 523 | arguments = Bundle().apply { 524 | putInt(ARG_SECTION_NUMBER, sectionNumber) 525 | } 526 | } 527 | } 528 | } 529 | 530 | override fun onDestroyView() { 531 | super.onDestroyView() 532 | _binding = null 533 | } 534 | } -------------------------------------------------------------------------------- /app/app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/activity_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 23 | 24 | 29 | 30 | 31 | 36 | -------------------------------------------------------------------------------- /app/app/src/main/res/layout/fragment_webview.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 30 | 31 | -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Guardsquare/android-insecure-tls-demo/a48017c24454c3596ad90c44f52976eaf328f486/app/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/app/src/main/res/raw/mitmtest_ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDfTCCAmWgAwIBAgIUHv31mWPY2IOZJVcUW/IcICgVPlgwDQYJKoZIhvcNAQEL 3 | BQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcM 4 | CVRoZSBDbG91ZDEWMBQGA1UECgwNTXkgQ29tcGFueSBDQTAeFw0yMjAxMTExMzU3 5 | MzdaFw0yMzAxMTExMzU3MzdaME4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp 6 | Zm9ybmlhMRIwEAYDVQQHDAlUaGUgQ2xvdWQxFjAUBgNVBAoMDU15IENvbXBhbnkg 7 | Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDskR848oIAlhOtq9CJ 8 | EFim/wX41ZMQWi7gvl9QD9Z2FGoImcGCKTPtbJYqJcQOrUSU38oRqAf73C9EJHaR 9 | YFS6YWsGiRwykftE0GdImr1wInlnOkc6wu7XvGKvjn8XY+OaMfIMgJWUjemJlX8b 10 | AfbxbjlJSJgDsNAFvjwzvqbryE102qqCRvF2+MHuc077NaJt6x9y7YAjx4eT26vv 11 | modSnHKekLY4uTumNP4yLGK/pUviqKGY5A9ufoHtMIJe9NGw3/OOXYrGbpol90G0 12 | mzlgIDeCev3dRmAct2sIzUsLmuhPIWuQ1GpO900bYSBz7YKlL7tvZmAQqXC3tc57 13 | ZD0ZAgMBAAGjUzBRMB0GA1UdDgQWBBR1+LLWgyUpcYm6iWdydsvDtaVjuTAfBgNV 14 | HSMEGDAWgBR1+LLWgyUpcYm6iWdydsvDtaVjuTAPBgNVHRMBAf8EBTADAQH/MA0G 15 | CSqGSIb3DQEBCwUAA4IBAQB3TDAPOCEDeN5swRixDvi5rgVcWFS5sOaELp7++SUW 16 | ah7sflJLBQF8D7jSRukoc12267lbdQkRvVKrX8uVBjdAewdmpv6u6fS6qA1KwOFe 17 | AMlM76xpFIH1uEpVfEw/VV284L9mvKoCccJ61b1shFtixdwfhurSDrcZG5jGZzZm 18 | 3PK5YdUgkzVah2hGy9tYsi7tvSPh2sV7qMD5Ww3Bk2sJH3aaST9iHbpWYgGy/e88 19 | VOT2Hsn1p5qDHM+COHnmmoLTlr5Ia2OaWRadm6LvzaRf6saD227VMSlRKAUIvjG7 20 | Vn68us4azQyXpzCEWIUuRxnTR0Td7sSBF3OYHUQ2C4EL 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /app/app/src/main/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /app/app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /app/app/src/main/res/values-w1240dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 200dp 3 | -------------------------------------------------------------------------------- /app/app/src/main/res/values-w600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /app/app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 16dp 7 | 8dp 8 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Insecure Communication 3 | WebviewActivity 4 | Abort 5 | -------------------------------------------------------------------------------- /app/app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |