Fix a crash if the View SSL Certificate is displayed when the app restarts. https...
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SslCertificateErrorDialog.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.Activity
24 import android.app.Dialog
25 import android.content.DialogInterface
26 import android.content.res.Configuration
27 import android.net.Uri
28 import android.net.http.SslError
29 import android.os.AsyncTask
30 import android.os.Bundle
31 import android.text.SpannableStringBuilder
32 import android.text.Spanned
33 import android.text.style.ForegroundColorSpan
34 import android.view.WindowManager
35 import android.widget.TextView
36
37 import androidx.appcompat.app.AlertDialog
38 import androidx.fragment.app.DialogFragment
39 import androidx.preference.PreferenceManager
40
41 import com.stoutner.privacybrowser.R
42 import com.stoutner.privacybrowser.activities.MainWebViewActivity
43 import com.stoutner.privacybrowser.views.NestedScrollWebView
44
45 import java.lang.ref.WeakReference
46 import java.net.InetAddress
47 import java.net.UnknownHostException
48 import java.text.DateFormat
49
50 // Define the class constants.
51 private const val PRIMARY_ERROR_INT = "primary_error_int"
52 private const val URL_WITH_ERRORS = "url_with_errors"
53 private const val ISSUED_TO_CNAME = "issued_to_cname"
54 private const val ISSUED_TO_ONAME = "issued_to_oname"
55 private const val ISSUED_TO_UNAME = "issued_to_uname"
56 private const val ISSUED_BY_CNAME = "issued_by_cname"
57 private const val ISSUED_BY_ONAME = "issued_by_oname"
58 private const val ISSUED_BY_UNAME = "issued_by_uname"
59 private const val START_DATE = "start_date"
60 private const val END_DATE = "end_date"
61 private const val WEBVIEW_FRAGMENT_ID = "webview_fragment_id"
62
63 class SslCertificateErrorDialog : DialogFragment() {
64     companion object {
65         // `@JvmStatic` will no longer be required once all the code has transitioned to Kotlin.
66         @JvmStatic
67         fun displayDialog(sslError: SslError, webViewFragmentId: Long): SslCertificateErrorDialog {
68             // Get the various components of the SSL error message.
69             val primaryErrorInt = sslError.primaryError
70             val urlWithErrors = sslError.url
71             val sslCertificate = sslError.certificate
72             val issuedToCName = sslCertificate.issuedTo.cName
73             val issuedToOName = sslCertificate.issuedTo.oName
74             val issuedToUName = sslCertificate.issuedTo.uName
75             val issuedByCName = sslCertificate.issuedBy.cName
76             val issuedByOName = sslCertificate.issuedBy.oName
77             val issuedByUName = sslCertificate.issuedBy.uName
78             val startDate = sslCertificate.validNotBeforeDate
79             val endDate = sslCertificate.validNotAfterDate
80
81             // Create an arguments bundle.
82             val argumentsBundle = Bundle()
83
84             // Store the SSL error message components in the bundle.
85             argumentsBundle.putInt(PRIMARY_ERROR_INT, primaryErrorInt)
86             argumentsBundle.putString(URL_WITH_ERRORS, urlWithErrors)
87             argumentsBundle.putString(ISSUED_TO_CNAME, issuedToCName)
88             argumentsBundle.putString(ISSUED_TO_ONAME, issuedToOName)
89             argumentsBundle.putString(ISSUED_TO_UNAME, issuedToUName)
90             argumentsBundle.putString(ISSUED_BY_CNAME, issuedByCName)
91             argumentsBundle.putString(ISSUED_BY_ONAME, issuedByOName)
92             argumentsBundle.putString(ISSUED_BY_UNAME, issuedByUName)
93             argumentsBundle.putString(START_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDate))
94             argumentsBundle.putString(END_DATE, DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDate))
95             argumentsBundle.putLong(WEBVIEW_FRAGMENT_ID, webViewFragmentId)
96
97             // Create a new instance of the SSL certificate error dialog.
98             val thisSslCertificateErrorDialog = SslCertificateErrorDialog()
99
100             // Add the arguments bundle to the new dialog.
101             thisSslCertificateErrorDialog.arguments = argumentsBundle
102
103             // Return the new dialog.
104             return thisSslCertificateErrorDialog
105         }
106     }
107
108     // `@SuppressLint("InflateParams")` removes the warning about using `null` as the parent view group when inflating the alert dialog.
109     @SuppressLint("InflateParams")
110     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
111         // Get the variables from the bundle.
112         val primaryErrorInt = requireArguments().getInt(PRIMARY_ERROR_INT)
113         val urlWithErrors = requireArguments().getString(URL_WITH_ERRORS)
114         val issuedToCName = requireArguments().getString(ISSUED_TO_CNAME)
115         val issuedToOName = requireArguments().getString(ISSUED_TO_ONAME)
116         val issuedToUName = requireArguments().getString(ISSUED_TO_UNAME)
117         val issuedByCName = requireArguments().getString(ISSUED_BY_CNAME)
118         val issuedByOName = requireArguments().getString(ISSUED_BY_ONAME)
119         val issuedByUName = requireArguments().getString(ISSUED_BY_UNAME)
120         val startDate = requireArguments().getString(START_DATE)
121         val endDate = requireArguments().getString(END_DATE)
122         val webViewFragmentId = requireArguments().getLong(WEBVIEW_FRAGMENT_ID)
123
124         // Get the current position of this WebView fragment.
125         val webViewPosition = MainWebViewActivity.webViewPagerAdapter.getPositionForId(webViewFragmentId)
126
127         // Get the WebView tab fragment.
128         val webViewTabFragment = MainWebViewActivity.webViewPagerAdapter.getPageFragment(webViewPosition)
129
130         // Get the fragment view.
131         val fragmentView = webViewTabFragment.requireView()
132
133         // Get a handle for the current WebView.
134         val nestedScrollWebView: NestedScrollWebView = fragmentView.findViewById(R.id.nestedscroll_webview)
135
136         // Get a handle for the SSL error handler.
137         val sslErrorHandler = nestedScrollWebView.sslErrorHandler
138
139         // Use an alert dialog builder to create the alert dialog.
140         val dialogBuilder = AlertDialog.Builder(requireContext(), R.style.PrivacyBrowserAlertDialog)
141
142         // Get the current theme status.
143         val currentThemeStatus = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
144
145         // Set the icon according to the theme.
146         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
147             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_day)
148         } else {
149             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_night)
150         }
151
152         // Set the title.
153         dialogBuilder.setTitle(R.string.ssl_certificate_error)
154
155         // Set the view.  The parent view is null because it will be assigned by the alert dialog.
156         dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null))
157
158         // Set a listener on the cancel button.
159         dialogBuilder.setNegativeButton(R.string.cancel) { _: DialogInterface?, _: Int ->
160             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
161             if (sslErrorHandler != null) {
162                 // Cancel the request.
163                 sslErrorHandler.cancel()
164
165                 // Reset the SSL error handler.
166                 nestedScrollWebView.resetSslErrorHandler()
167             }
168         }
169
170         // Set a listener on the proceed button.
171         dialogBuilder.setPositiveButton(R.string.proceed) { _: DialogInterface?, _: Int ->
172             // Check to make sure the SSL error handler is not null.  This might happen if multiple dialogs are displayed at once.
173             if (sslErrorHandler != null) {
174                 // Proceed to the website.
175                 sslErrorHandler.proceed()
176
177                 // Reset the SSL error handler.
178                 nestedScrollWebView.resetSslErrorHandler()
179             }
180         }
181
182         // Create an alert dialog from the builder.
183         val alertDialog = dialogBuilder.create()
184
185         // Get a handle for the shared preferences.
186         val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
187
188         // Get the screenshot preference.
189         val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
190
191         // Disable screenshots if not allowed.
192         if (!allowScreenshots) {
193             // Disable screenshots.
194             alertDialog.window!!.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
195         }
196
197         // Get a URI for the URL with errors.
198         val uriWithErrors = Uri.parse(urlWithErrors)
199
200         // Get the IP addresses for the URI.
201         GetIpAddresses(requireActivity(), alertDialog).execute(uriWithErrors.host)
202
203         // The alert dialog must be shown before the contents can be modified.
204         alertDialog.show()
205
206         // Get handles for the views.
207         val primaryErrorTextView = alertDialog.findViewById<TextView>(R.id.primary_error)!!
208         val urlTextView = alertDialog.findViewById<TextView>(R.id.url)!!
209         val issuedToCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_cname)!!
210         val issuedToONameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_oname)!!
211         val issuedToUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_to_uname)!!
212         val issuedByTextView = alertDialog.findViewById<TextView>(R.id.issued_by_textview)!!
213         val issuedByCNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_cname)!!
214         val issuedByONameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_oname)!!
215         val issuedByUNameTextView = alertDialog.findViewById<TextView>(R.id.issued_by_uname)!!
216         val validDatesTextView = alertDialog.findViewById<TextView>(R.id.valid_dates_textview)!!
217         val startDateTextView = alertDialog.findViewById<TextView>(R.id.start_date)!!
218         val endDateTextView = alertDialog.findViewById<TextView>(R.id.end_date)!!
219
220         // Setup the common strings.
221         val urlLabel = getString(R.string.url_label) + "  "
222         val cNameLabel = getString(R.string.common_name) + "  "
223         val oNameLabel = getString(R.string.organization) + "  "
224         val uNameLabel = getString(R.string.organizational_unit) + "  "
225         val startDateLabel = getString(R.string.start_date) + "  "
226         val endDateLabel = getString(R.string.end_date) + "  "
227
228         // Create a spannable string builder for each text view that needs multiple colors of text.
229         val urlStringBuilder = SpannableStringBuilder(urlLabel + urlWithErrors)
230         val issuedToCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedToCName)
231         val issuedToONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedToOName)
232         val issuedToUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedToUName)
233         val issuedByCNameStringBuilder = SpannableStringBuilder(cNameLabel + issuedByCName)
234         val issuedByONameStringBuilder = SpannableStringBuilder(oNameLabel + issuedByOName)
235         val issuedByUNameStringBuilder = SpannableStringBuilder(uNameLabel + issuedByUName)
236         val startDateStringBuilder = SpannableStringBuilder(startDateLabel + startDate)
237         val endDateStringBuilder = SpannableStringBuilder(endDateLabel + endDate)
238
239         // Define the color spans.
240         val blueColorSpan: ForegroundColorSpan
241         val redColorSpan: ForegroundColorSpan
242
243         // Set the color spans according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
244         if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
245             @Suppress("DEPRECATION")
246             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.blue_700))
247             @Suppress("DEPRECATION")
248             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_a700))
249         } else {
250             @Suppress("DEPRECATION")
251             blueColorSpan = ForegroundColorSpan(resources.getColor(R.color.violet_700))
252             @Suppress("DEPRECATION")
253             redColorSpan = ForegroundColorSpan(resources.getColor(R.color.red_900))
254         }
255
256         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
257         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
258         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
259         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedToONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
260         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedToUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
261         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
262         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
263         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
264         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
265         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
266
267         // Define the primary error string.
268         var primaryErrorString = ""
269
270         // Highlight the primary error in red and store it in the primary error string.
271         when (primaryErrorInt) {
272             SslError.SSL_IDMISMATCH -> {
273                 // Change the URL span colors to red.
274                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length, urlStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
275                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedToCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
276
277                 // Store the primary error string.
278                 primaryErrorString = getString(R.string.cn_mismatch)
279             }
280
281             SslError.SSL_UNTRUSTED -> {
282                 // Change the issued by text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
283                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
284                     @Suppress("DEPRECATION")
285                     issuedByTextView.setTextColor(resources.getColor(R.color.red_a700))
286                 } else {
287                     @Suppress("DEPRECATION")
288                     issuedByTextView.setTextColor(resources.getColor(R.color.red_900))
289                 }
290
291                 // Change the issued by span color to red.
292                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length, issuedByCNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
293                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length, issuedByONameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
294                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length, issuedByUNameStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
295
296                 // Store the primary error string.
297                 primaryErrorString = getString(R.string.untrusted)
298             }
299
300             SslError.SSL_DATE_INVALID -> {
301                 // Change the valid dates text view text to red.  The deprecated `getColor()` must be used until the minimum API >= 23.
302                 if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
303                     @Suppress("DEPRECATION")
304                     validDatesTextView.setTextColor(resources.getColor(R.color.red_a700))
305                 } else {
306                     @Suppress("DEPRECATION")
307                     validDatesTextView.setTextColor(resources.getColor(R.color.red_900))
308                 }
309
310                 // Change the date span colors to red.
311                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
312                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
313
314                 // Store the primary error string.
315                 primaryErrorString = getString(R.string.invalid_date)
316             }
317
318             SslError.SSL_NOTYETVALID -> {
319                 // Change the start date span color to red.
320                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length, startDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
321
322                 // Store the primary error string.
323                 primaryErrorString = getString(R.string.future_certificate)
324             }
325
326             SslError.SSL_EXPIRED -> {
327                 // Change the end date span color to red.
328                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length, endDateStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
329
330                 // Store the primary error string.
331                 primaryErrorString = getString(R.string.expired_certificate)
332             }
333
334             SslError.SSL_INVALID ->
335                 // Store the primary error string.
336                 primaryErrorString = getString(R.string.invalid_certificate)
337         }
338
339         // Display the strings.
340         primaryErrorTextView.text = primaryErrorString
341         urlTextView.text = urlStringBuilder
342         issuedToCNameTextView.text = issuedToCNameStringBuilder
343         issuedToONameTextView.text = issuedToONameStringBuilder
344         issuedToUNameTextView.text = issuedToUNameStringBuilder
345         issuedByCNameTextView.text = issuedByCNameStringBuilder
346         issuedByONameTextView.text = issuedByONameStringBuilder
347         issuedByUNameTextView.text = issuedByUNameStringBuilder
348         startDateTextView.text = startDateStringBuilder
349         endDateTextView.text = endDateStringBuilder
350
351         // Return the alert dialog.
352         return alertDialog
353     }
354
355     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
356     private class GetIpAddresses constructor(activity: Activity, alertDialog: AlertDialog) : AsyncTask<String, Void?, SpannableStringBuilder>() {
357         // Define the weak references.
358         private val activityWeakReference: WeakReference<Activity> = WeakReference(activity)
359         private val alertDialogWeakReference: WeakReference<AlertDialog> = WeakReference(alertDialog)
360
361         override fun doInBackground(vararg domainName: String): SpannableStringBuilder {
362             // Get handles for the activity and the alert dialog.
363             val activity = activityWeakReference.get()
364             val alertDialog = alertDialogWeakReference.get()
365
366             // Abort if the activity or the dialog is gone.
367             if (activity == null || activity.isFinishing || alertDialog == null) {
368                 return SpannableStringBuilder()
369             }
370
371             // Initialize an IP address string builder.
372             val ipAddresses = StringBuilder()
373
374             // Get an array with the IP addresses for the host.
375             try {
376                 // Get an array with all the IP addresses for the domain.
377                 val inetAddressesArray = InetAddress.getAllByName(domainName[0])
378
379                 // Add each IP address to the string builder.
380                 for (inetAddress in inetAddressesArray) {
381                     // Check to see if this is not the first IP address.
382                     if (ipAddresses.isNotEmpty()) {
383                         // Add a line break to the string builder first.
384                         ipAddresses.append("\n")
385                     }
386
387                     // Add the IP Address to the string builder.
388                     ipAddresses.append(inetAddress.hostAddress)
389                 }
390             } catch (exception: UnknownHostException) {
391                 // Do nothing.
392             }
393
394             // Set the label.
395             val ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  "
396
397             // Create a spannable string builder.
398             val ipAddressesStringBuilder = SpannableStringBuilder(ipAddressesLabel + ipAddresses)
399
400             // Create a blue foreground color span.
401             val blueColorSpan: ForegroundColorSpan
402
403             // Get the current theme status.
404             val currentThemeStatus = activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
405
406             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
407             blueColorSpan = if (currentThemeStatus == Configuration.UI_MODE_NIGHT_NO) {
408                 @Suppress("DEPRECATION")
409                 ForegroundColorSpan(activity.resources.getColor(R.color.blue_700))
410             } else {
411                 @Suppress("DEPRECATION")
412                 ForegroundColorSpan(activity.resources.getColor(R.color.violet_500))
413             }
414
415             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
416             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length, ipAddressesStringBuilder.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
417
418             // Return the formatted string.
419             return ipAddressesStringBuilder
420         }
421
422         // `onPostExecute()` operates on the UI thread.
423         override fun onPostExecute(ipAddresses: SpannableStringBuilder) {
424             // Get handles for the activity and the alert dialog.
425             val activity = activityWeakReference.get()
426             val alertDialog = alertDialogWeakReference.get()
427
428             // Abort if the activity or the alert dialog is gone.
429             if (activity == null || activity.isFinishing || alertDialog == null) {
430                 return
431             }
432
433             // Get a handle for the IP addresses text view.
434             val ipAddressesTextView = alertDialog.findViewById<TextView>(R.id.ip_addresses)!!
435
436             // Populate the IP addresses text view.
437             ipAddressesTextView.text = ipAddresses
438         }
439     }
440 }