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