]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/dialogs/ViewSslCertificateDialog.kt
fbbfdabd20bd32e39670109c866896cdea310a31
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / ViewSslCertificateDialog.kt
1 /*
2  * Copyright © 2016-2021 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.dialogs
21
22 import android.annotation.SuppressLint
23 import android.app.Dialog
24 import android.content.res.Configuration
25 import android.graphics.Bitmap
26 import android.graphics.BitmapFactory
27 import android.graphics.drawable.BitmapDrawable
28 import android.net.Uri
29 import android.os.Bundle
30 import android.text.SpannableStringBuilder
31 import android.text.Spanned
32 import android.text.style.ForegroundColorSpan
33 import android.view.WindowManager
34 import android.widget.TextView
35
36 import androidx.appcompat.app.AlertDialog
37 import androidx.fragment.app.DialogFragment
38 import androidx.preference.PreferenceManager
39
40 import com.stoutner.privacybrowser.R
41 import com.stoutner.privacybrowser.activities.MainWebViewActivity
42 import com.stoutner.privacybrowser.views.NestedScrollWebView
43
44 import java.io.ByteArrayOutputStream
45
46 import java.text.DateFormat
47 import java.util.Calendar
48 import java.util.Date
49
50 // Define the class constants.
51 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
52 private const val FAVORITE_ICON_BYTE_ARRAY = "favorite_icon_byte_array"
53 private const val HAS_SSL_CERTIFICATE = "has_ssl_certificate"
54 private const val DOMAIN = "domain"
55 private const val IP_ADDRESSES = "ip_addresses"
56 private const val ISSUED_TO_CNAME = "issued_to_cname"
57 private const val ISSUED_TO_ONAME = "issued_to_oname"
58 private const val ISSUED_TO_UNAME = "issued_to_uname"
59 private const val ISSUED_BY_CNAME = "issued_by_cname"
60 private const val ISSUED_BY_ONAME = "issued_by_oname"
61 private const val ISSUED_BY_UNAME = "issued_by_uname"
62 private const val START_DATE = "start_date"
63 private const val END_DATE = "end_date"
64
65 class ViewSslCertificateDialog : DialogFragment() {
66     // Define the class variables.
67     private var hasSslCertificate: Boolean = false
68
69     // Declare the class variables.
70     private lateinit var domainString: String
71     private lateinit var ipAddresses: String
72     private lateinit var issuedToCName: String
73     private lateinit var issuedToOName: String
74     private lateinit var issuedToUName: String
75     private lateinit var issuedByCName: String
76     private lateinit var issuedByOName: String
77     private lateinit var issuedByUName: String
78     private lateinit var startDate: Date
79     private lateinit var endDate: Date
80
81     // Declare the class views.
82     private lateinit var nestedScrollWebView: NestedScrollWebView
83
84     companion object {
85         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
86         @JvmStatic
87         fun displayDialog(webViewFragmentId: Long, favoriteIconBitmap: Bitmap): ViewSslCertificateDialog {
88             // Create an arguments bundle.
89             val argumentsBundle = Bundle()
90
91             // Create a favorite icon byte array output stream.
92             val favoriteIconByteArrayOutputStream = ByteArrayOutputStream()
93
94             // Convert the bitmap to a PNG and place it in the byte array output stream.  `0` is for lossless compression (the only option for a PNG).
95             favoriteIconBitmap.compress(Bitmap.CompressFormat.PNG, 0, favoriteIconByteArrayOutputStream)
96
97             // Convert the favorite icon byte array output stream to a byte array.
98             val favoriteIconByteArray = favoriteIconByteArrayOutputStream.toByteArray()
99
100             // Store the arguments in the bundle.
101             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
102             argumentsBundle.putByteArray(FAVORITE_ICON_BYTE_ARRAY, favoriteIconByteArray)
103
104             // Create a new instance of the view SSL certificate dialog.
105             val viewSslCertificateDialog = ViewSslCertificateDialog()
106
107             // Add the bundle to the new dialog.
108             viewSslCertificateDialog.arguments = argumentsBundle
109
110             // Return the new dialog.
111             return viewSslCertificateDialog
112         }
113     }
114
115     // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
116     @SuppressLint("InflateParams")
117     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
118         // Use a builder to create the alert dialog.
119         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
120
121         // Populate the class variables.
122         if (savedInstanceState == null) {  // The dialog is starting for the first time.
123             // Get the current position of this WebView fragment.
124             val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(requireArguments().getLong(WEBVIEW_FRAGMENT_ID))
125
126             // Get the WebView tab fragment.
127             val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
128
129             // Get the fragment view.
130             val fragmentView = webViewTabFragment.requireView()
131
132             // Get a handle for the current nested scroll WebView.
133             nestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
134
135             // Get the SSL certificate.
136             val sslCertificate = nestedScrollWebView.certificate
137
138             // Store the status of the SSL certificate.
139             hasSslCertificate = sslCertificate != null
140
141             // Populate the certificate class variables if the webpage has an SSL certificate.
142             if (hasSslCertificate) {
143                 // Convert the URL to a URI.
144                 val uri = Uri.parse(nestedScrollWebView.url)
145
146                 // Extract the domain name from the URI.
147                 domainString = uri.host!!
148
149                 // Get the ip addresses from the nested scroll WebView.
150                 ipAddresses = nestedScrollWebView.currentIpAddresses
151
152                 // Get the strings from the SSL certificate.
153                 issuedToCName = sslCertificate!!.issuedTo.cName
154                 issuedToOName = sslCertificate.issuedTo.oName
155                 issuedToUName = sslCertificate.issuedTo.uName
156                 issuedByCName = sslCertificate.issuedBy.cName
157                 issuedByOName = sslCertificate.issuedBy.oName
158                 issuedByUName = sslCertificate.issuedBy.uName
159                 startDate = sslCertificate.validNotBeforeDate
160                 endDate = sslCertificate.validNotAfterDate
161             }
162         } else {  // The dialog has been restarted.
163             // Get the data from the saved instance state.
164             hasSslCertificate = savedInstanceState.getBoolean(HAS_SSL_CERTIFICATE)
165
166             // Populate the certificate class variables if the webpage has an SSL certificate.
167             if (hasSslCertificate) {
168                 // Populate the certificate class variables from the saved instance state.
169                 domainString = savedInstanceState.getString(DOMAIN)!!
170                 ipAddresses = savedInstanceState.getString(IP_ADDRESSES)!!
171                 issuedToCName = savedInstanceState.getString(ISSUED_TO_CNAME)!!
172                 issuedToOName = savedInstanceState.getString(ISSUED_TO_ONAME)!!
173                 issuedToUName = savedInstanceState.getString(ISSUED_TO_UNAME)!!
174                 issuedByCName = savedInstanceState.getString(ISSUED_BY_CNAME)!!
175                 issuedByOName = savedInstanceState.getString(ISSUED_BY_ONAME)!!
176                 issuedByUName = savedInstanceState.getString(ISSUED_BY_UNAME)!!
177                 startDate = Date(savedInstanceState.getLong(START_DATE))
178                 endDate = Date(savedInstanceState.getLong(END_DATE))
179             }
180         }
181
182         // Get the favorite icon byte array from the arguments.
183         val favoriteIconByteArray = requireArguments().getByteArray(FAVORITE_ICON_BYTE_ARRAY)!!
184
185         // Convert the favorite icon byte array to a bitmap.
186         val favoriteIconBitmap = BitmapFactory.decodeByteArray(favoriteIconByteArray, 0, favoriteIconByteArray.size)
187
188         // Create a drawable version of the favorite icon.
189         val favoriteIconDrawable = BitmapDrawable(resources, favoriteIconBitmap)
190
191         // Set the icon.
192         dialogBuilder.setIcon(favoriteIconDrawable)
193
194         // Set the close button listener.  Using `null` as the listener closes the dialog without doing anything else.
195         dialogBuilder.setNegativeButton(R.string.close, null)
196
197         // Get a handle for the shared preferences.
198         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
199
200         // Get the screenshot preference.
201         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
202
203         // Check to see if the website is encrypted.
204         if (hasSslCertificate) {  // The website is encrypted.
205             // Set the title.
206             dialogBuilder.setTitle(R.string.ssl_certificate)
207
208             // Set the layout.  The parent view is `null` because it will be assigned by the alert dialog.
209             dialogBuilder.setView(layoutInflater.inflate(R.layout.view_ssl_certificate_dialog, null))
210
211             // Create an alert dialog from the builder.
212             val alertDialog = dialogBuilder.create()
213
214             // Disable screenshots if not allowed.
215             if (!allowScreenshots) {
216                 // Disable screenshots.
217                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
218             }
219
220             // The alert dialog must be shown before items in the layout can be modified.
221             alertDialog.show()
222
223             // Get handles for the text views.
224             val domainTextView = alertDialog.findViewById<TextView>(R.id.domain)!!
225             val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
226             val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
227             val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
228             val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
229             val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
230             val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
231             val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
232             val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
233             val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
234
235             // Setup the labels.
236             val domainLabel = getString(R.string.domain_label) + "  "
237             val ipAddressesLabel = getString(R.string.ip_addresses) + "  "
238             val cNameLabel = getString(R.string.common_name) + "  "
239             val oNameLabel = getString(R.string.organization) + "  "
240             val uNameLabel = getString(R.string.organizational_unit) + "  "
241             val startDateLabel = getString(R.string.start_date) + "  "
242             val endDateLabel = getString(R.string.end_date) + "  "
243
244             // Create spannable string builders for each text view that needs multiple colors of text.
245             val domainStringBuilder = SpannableStringBuilder(domainLabel + domainString)
246             val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
247             val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
248             val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
249             val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
250             val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
251             val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
252             val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
253             val startDateStringBuilder = SpannableStringBuilder(startDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
254             val endDateStringBuilder = SpannableStringBuilder(endDateLabel + DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
255
256             // Define the color spans.
257             val blueColorSpan: ForegroundColorSpan
258             val redColorSpan: ForegroundColorSpan
259
260             // Get the current theme status.
261             val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
262
263             // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
264             if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
265                 @Suppress("DEPRECATION")
266                 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
267                 @Suppress("DEPRECATION")
268                 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
269             } else {
270                 @Suppress("DEPRECATION")
271                 blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
272                 @Suppress("DEPRECATION")
273                 redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
274             }
275
276             // Format the domain string and issued to CName colors.
277             if (domainString == issuedToCName) {  // The domain and issued to CName match.
278                 // Set the strings to be blue.
279                 domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
280                 issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
281             } else if (issuedToCName.startsWith("*.")) {  // The issued to CName begins with a wildcard.
282                 // Remove the initial `*.`.
283                 val baseCertificateDomain = issuedToCName.substring(2)
284
285                 // Setup a copy of the domain string to test subdomains.
286                 var domainStringSubdomain = domainString
287
288                 // Define a domain names match variable.
289                 var domainNamesMatch = false
290
291                 // Check all the subdomains against the base certificate domain.
292                 while (!domainNamesMatch && domainStringSubdomain.contains(".")) {  // Stop checking if we know that the domain names match or if we run out of subdomains.
293                     // Test the subdomain against the base certificate domain.
294                     if (domainStringSubdomain == baseCertificateDomain) {
295                         domainNamesMatch = true
296                     }
297
298                     // Strip out the lowest subdomain.
299                     domainStringSubdomain = domainStringSubdomain.substring(domainStringSubdomain.indexOf(".") + 1)
300                 }
301
302                 // Format the domain and issued to CName.
303                 if (domainNamesMatch) {  // The domain is a subdomain of the wildcard certificate.
304                     // Set the strings to be blue.
305                     domainStringBuilder.setSpan(blueColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
306                     issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
307                 } else {  // The domain is not a subdomain of the wildcard certificate.
308                     // Set the string to be red.
309                     domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
310                     issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
311                 }
312             } else {  // The strings do not match and issued to CName does not begin with a wildcard.
313                 // Set the strings to be red.
314                 domainStringBuilder.setSpan(redColorSpan, domainLabel.length, domainStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
315                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
316             }
317
318             // Set the IP addresses, issued to, and issued by spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
319             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
320             issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321             issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
322             issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
323             issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
324             issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
325
326             // Get the current date.
327             val currentDate = Calendar.getInstance().time
328
329             //  Format the start date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
330             if (startDate.after(currentDate)) {  // The certificate start date is in the future.
331                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
332             } else {  // The certificate start date is in the past.
333                 startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
334             }
335
336             // Format the end date color.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
337             if (endDate.before(currentDate)) {  // The certificate end date is in the past.
338                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
339             } else {  // The certificate end date is in the future.
340                 endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
341             }
342
343             // Display the strings.
344             domainTextView.text = domainStringBuilder
345             ipAddressesTextView.text = ipAddressesStringBuilder
346             issuedToCNameTextView.text = issuedToCNameStringBuilder
347             issuedToONameTextView.text = issuedToONameStringBuilder
348             issuedToUNameTextView.text = issuedToUNameStringBuilder
349             issuedByCNameTextView.text = issuedByCNameStringBuilder
350             issuedByONameTextView.text = issuedByONameStringBuilder
351             issuedByUNameTextView.text = issuedByUNameStringBuilder
352             startDateTextView.text = startDateStringBuilder
353             endDateTextView.text = endDateStringBuilder
354
355             // Return the alert dialog.
356             return alertDialog
357         } else {  // The website is not encrypted.
358             // Set the title.
359             dialogBuilder.setTitle(R.string.unencrypted_website)
360
361             // Set the Layout.  The parent view is `null` because it will be assigned by the alert dialog.
362             dialogBuilder.setView(layoutInflater.inflate(R.layout.unencrypted_website_dialog, null))
363
364             // Create an alert dialog from the builder.
365             val alertDialog = dialogBuilder.create()
366
367             // Disable screenshots if not allowed.
368             if (!allowScreenshots) {
369                 // Disable screenshots.
370                 alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
371             }
372
373             // Return the alert dialog.
374             return alertDialog
375         }
376     }
377
378     override fun onSaveInstanceState(savedInstanceState: Bundle) {
379         // Run the default commands.
380         super.onSaveInstanceState(savedInstanceState)
381
382         // Save the common class variables.
383         savedInstanceState.putBoolean(HAS_SSL_CERTIFICATE, hasSslCertificate)
384
385         // Save the SSL certificate strings if they exist.
386         if (hasSslCertificate) {
387             savedInstanceState.putString(DOMAIN, domainString)
388             savedInstanceState.putString(IP_ADDRESSES, ipAddresses)
389             savedInstanceState.putString(ISSUED_TO_CNAME, issuedToCName)
390             savedInstanceState.putString(ISSUED_TO_ONAME, issuedToOName)
391             savedInstanceState.putString(ISSUED_TO_UNAME, issuedToUName)
392             savedInstanceState.putString(ISSUED_BY_CNAME, issuedByCName)
393             savedInstanceState.putString(ISSUED_BY_ONAME, issuedByOName)
394             savedInstanceState.putString(ISSUED_BY_UNAME, issuedByUName)
395             savedInstanceState.putLong(START_DATE, startDate.time)
396             savedInstanceState.putLong(END_DATE, endDate.time)
397         }
398     }
399 }