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