5f46d4f13e42524ab9b507ef70cdad5a545f81f8
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / dialogs / SslCertificateErrorDialog.java
1 /*
2  * Copyright © 2016-2019 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.AlertDialog;
25 import android.app.Dialog;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.SharedPreferences;
29 import android.net.Uri;
30 import android.net.http.SslCertificate;
31 import android.net.http.SslError;
32 import android.os.AsyncTask;
33 import android.os.Bundle;
34 import android.preference.PreferenceManager;
35 import android.text.SpannableStringBuilder;
36 import android.text.Spanned;
37 import android.text.style.ForegroundColorSpan;
38 import android.view.LayoutInflater;
39 import android.view.WindowManager;
40 import android.widget.TextView;
41
42 import androidx.annotation.NonNull;
43 import androidx.fragment.app.DialogFragment;  // The AndroidX dialog fragment must be used or an error is produced on API <=22.
44
45 import com.stoutner.privacybrowser.R;
46
47 import java.lang.ref.WeakReference;
48 import java.net.InetAddress;
49 import java.net.UnknownHostException;
50 import java.text.DateFormat;
51 import java.util.Date;
52
53 public class SslCertificateErrorDialog extends DialogFragment {
54     // `sslCertificateErrorListener` is used in `onAttach` and `onCreateDialog`.
55     private SslCertificateErrorListener sslCertificateErrorListener;
56
57     // The public interface is used to send information back to the parent activity.
58     public interface SslCertificateErrorListener {
59         void onSslErrorCancel();
60
61         void onSslErrorProceed();
62     }
63
64     public void onAttach(Context context) {
65         // Run the default commands.
66         super.onAttach(context);
67
68         // Get a handle for `SslCertificateErrorListener` from the launching context.
69         sslCertificateErrorListener = (SslCertificateErrorListener) context;
70     }
71
72     public static SslCertificateErrorDialog displayDialog(SslError error) {
73         // Get the various components of the SSL error message.
74         int primaryErrorIntForBundle = error.getPrimaryError();
75         String urlWithErrorForBundle = error.getUrl();
76         SslCertificate sslCertificate = error.getCertificate();
77         String issuedToCNameForBundle = sslCertificate.getIssuedTo().getCName();
78         String issuedToONameForBundle = sslCertificate.getIssuedTo().getOName();
79         String issuedToUNameForBundle = sslCertificate.getIssuedTo().getUName();
80         String issuedByCNameForBundle = sslCertificate.getIssuedBy().getCName();
81         String issuedByONameForBundle = sslCertificate.getIssuedBy().getOName();
82         String issuedByUNameForBundle = sslCertificate.getIssuedBy().getUName();
83         Date startDateForBundle = sslCertificate.getValidNotBeforeDate();
84         Date endDateForBundle = sslCertificate.getValidNotAfterDate();
85
86         // Store the SSL error message components in a `Bundle`.
87         Bundle argumentsBundle = new Bundle();
88         argumentsBundle.putInt("PrimaryErrorInt", primaryErrorIntForBundle);
89         argumentsBundle.putString("UrlWithError", urlWithErrorForBundle);
90         argumentsBundle.putString("IssuedToCName", issuedToCNameForBundle);
91         argumentsBundle.putString("IssuedToOName", issuedToONameForBundle);
92         argumentsBundle.putString("IssuedToUName", issuedToUNameForBundle);
93         argumentsBundle.putString("IssuedByCName", issuedByCNameForBundle);
94         argumentsBundle.putString("IssuedByOName", issuedByONameForBundle);
95         argumentsBundle.putString("IssuedByUName", issuedByUNameForBundle);
96         argumentsBundle.putString("StartDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(startDateForBundle));
97         argumentsBundle.putString("EndDate", DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.LONG).format(endDateForBundle));
98
99         // Add `argumentsBundle` to this instance of `SslCertificateErrorDialog`.
100         SslCertificateErrorDialog thisSslCertificateErrorDialog = new SslCertificateErrorDialog();
101         thisSslCertificateErrorDialog.setArguments(argumentsBundle);
102         return thisSslCertificateErrorDialog;
103     }
104
105     // `@SuppressLing("InflateParams")` removes the warning about using `null` as the parent view group when inflating the `AlertDialog`.
106     @SuppressLint("InflateParams")
107     @SuppressWarnings("deprecation")
108     @Override
109     @NonNull
110     public Dialog onCreateDialog(Bundle savedInstanceState) {
111         // Remove the incorrect lint warning that `getArguments()` might be null.
112         assert getArguments() != null;
113
114         // Get the components of the SSL error message from the bundle.
115         int primaryErrorInt = getArguments().getInt("PrimaryErrorInt");
116         String urlWithErrors = getArguments().getString("UrlWithError");
117         String issuedToCName = getArguments().getString("IssuedToCName");
118         String issuedToOName = getArguments().getString("IssuedToOName");
119         String issuedToUName = getArguments().getString("IssuedToUName");
120         String issuedByCName = getArguments().getString("IssuedByCName");
121         String issuedByOName = getArguments().getString("IssuedByOName");
122         String issuedByUName = getArguments().getString("IssuedByUName");
123         String startDate = getArguments().getString("StartDate");
124         String endDate = getArguments().getString("EndDate");
125
126         // Remove the incorrect lint warning that `getActivity()` might be null.
127         assert getActivity() != null;
128
129         // Get the activity's layout inflater.
130         LayoutInflater layoutInflater = getActivity().getLayoutInflater();
131
132         // Use an alert dialog builder to create the alert dialog.
133         AlertDialog.Builder dialogBuilder;
134
135         // Get a handle for the shared preferences.
136         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
137
138         // Get the screenshot and theme preferences.
139         boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
140         boolean allowScreenshots = sharedPreferences.getBoolean("allow_screenshots", false);
141
142         // Set the style and icon according to the theme.
143         if (darkTheme) {
144             // Set the style.
145             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogDark);
146
147             // Set the icon.
148             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_dark);
149         } else {
150             // Set the style.
151             dialogBuilder = new AlertDialog.Builder(getActivity(), R.style.PrivacyBrowserAlertDialogLight);
152
153             // Set the icon.
154             dialogBuilder.setIcon(R.drawable.ssl_certificate_enabled_light);
155         }
156
157         // Set the title.
158         dialogBuilder.setTitle(R.string.ssl_certificate_error);
159
160         // Set the view.  The parent view is `null` because it will be assigned by `AlertDialog`.
161         dialogBuilder.setView(layoutInflater.inflate(R.layout.ssl_certificate_error, null));
162
163         // Set a listener on the negative button.
164         dialogBuilder.setNegativeButton(R.string.cancel, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorCancel());
165
166         // Set a listener on the positive button.
167         dialogBuilder.setPositiveButton(R.string.proceed, (DialogInterface dialog, int which) -> sslCertificateErrorListener.onSslErrorProceed());
168
169
170         // Create an alert dialog from the alert dialog builder.
171         AlertDialog alertDialog = dialogBuilder.create();
172
173         // Disable screenshots if not allowed.
174         if (!allowScreenshots) {
175             // Remove the warning below that `getWindow()` might be null.
176             assert alertDialog.getWindow() != null;
177
178             // Disable screenshots.
179             alertDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
180         }
181
182         // Get a URI for the URL with errors.
183         Uri uriWithErrors = Uri.parse(urlWithErrors);
184
185         // Get the IP addresses for the URI.
186         new GetIpAddresses(getActivity(), alertDialog).execute(uriWithErrors.getHost());
187
188         // The alert dialog must be shown before the contents can be modified.
189         alertDialog.show();
190
191         // Get handles for the `TextViews`
192         TextView primaryErrorTextView = alertDialog.findViewById(R.id.primary_error);
193         TextView urlTextView = alertDialog.findViewById(R.id.url);
194         TextView issuedToCNameTextView = alertDialog.findViewById(R.id.issued_to_cname);
195         TextView issuedToONameTextView = alertDialog.findViewById(R.id.issued_to_oname);
196         TextView issuedToUNameTextView = alertDialog.findViewById(R.id.issued_to_uname);
197         TextView issuedByTextView = alertDialog.findViewById(R.id.issued_by_textview);
198         TextView issuedByCNameTextView = alertDialog.findViewById(R.id.issued_by_cname);
199         TextView issuedByONameTextView = alertDialog.findViewById(R.id.issued_by_oname);
200         TextView issuedByUNameTextView = alertDialog.findViewById(R.id.issued_by_uname);
201         TextView validDatesTextView = alertDialog.findViewById(R.id.valid_dates_textview);
202         TextView startDateTextView = alertDialog.findViewById(R.id.start_date);
203         TextView endDateTextView = alertDialog.findViewById(R.id.end_date);
204
205         // Setup the common strings.
206         String urlLabel = getString(R.string.url_label) + "  ";
207         String cNameLabel = getString(R.string.common_name) + "  ";
208         String oNameLabel = getString(R.string.organization) + "  ";
209         String uNameLabel = getString(R.string.organizational_unit) + "  ";
210         String startDateLabel = getString(R.string.start_date) + "  ";
211         String endDateLabel = getString(R.string.end_date) + "  ";
212
213         // Create a spannable string builder for each text view that needs multiple colors of text.
214         SpannableStringBuilder urlStringBuilder = new SpannableStringBuilder(urlLabel + urlWithErrors);
215         SpannableStringBuilder issuedToCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedToCName);
216         SpannableStringBuilder issuedToONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedToOName);
217         SpannableStringBuilder issuedToUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedToUName);
218         SpannableStringBuilder issuedByCNameStringBuilder = new SpannableStringBuilder(cNameLabel + issuedByCName);
219         SpannableStringBuilder issuedByONameStringBuilder = new SpannableStringBuilder(oNameLabel + issuedByOName);
220         SpannableStringBuilder issuedByUNameStringBuilder = new SpannableStringBuilder(uNameLabel + issuedByUName);
221         SpannableStringBuilder startDateStringBuilder = new SpannableStringBuilder(startDateLabel + startDate);
222         SpannableStringBuilder endDateStringBuilder = new SpannableStringBuilder((endDateLabel + endDate));
223
224         // Create a red foreground color span.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
225         @SuppressWarnings("deprecation") ForegroundColorSpan redColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.red_a700));
226
227         // Create a blue `ForegroundColorSpan`.
228         ForegroundColorSpan blueColorSpan;
229
230         // Set a blue color span according to the theme.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
231         if (darkTheme) {
232             //noinspection deprecation
233             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_400));
234         } else {
235             //noinspection deprecation
236             blueColorSpan = new ForegroundColorSpan(getResources().getColor(R.color.blue_700));
237         }
238
239         // Setup the spans to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
240         urlStringBuilder.setSpan(blueColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
241         issuedToCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
242         issuedToONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedToONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
243         issuedToUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedToUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
244         issuedByCNameStringBuilder.setSpan(blueColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
245         issuedByONameStringBuilder.setSpan(blueColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
246         issuedByUNameStringBuilder.setSpan(blueColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
247         startDateStringBuilder.setSpan(blueColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
248         endDateStringBuilder.setSpan(blueColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
249
250         // Initialize `primaryErrorString`.
251         String primaryErrorString = "";
252
253         // Highlight the primary error in red and store the primary error string in `primaryErrorString`.
254         switch (primaryErrorInt) {
255             case SslError.SSL_IDMISMATCH:
256                 // Change the URL span colors to red.
257                 urlStringBuilder.setSpan(redColorSpan, urlLabel.length(), urlStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
258                 issuedToCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedToCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
259
260                 // Store the primary error string.
261                 primaryErrorString = getString(R.string.cn_mismatch);
262                 break;
263
264             case SslError.SSL_UNTRUSTED:
265                 // Change the issued by text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
266                 issuedByTextView.setTextColor(getResources().getColor(R.color.red_a700));
267
268                 // Change the issued by span color to red.
269                 issuedByCNameStringBuilder.setSpan(redColorSpan, cNameLabel.length(), issuedByCNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
270                 issuedByONameStringBuilder.setSpan(redColorSpan, oNameLabel.length(), issuedByONameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
271                 issuedByUNameStringBuilder.setSpan(redColorSpan, uNameLabel.length(), issuedByUNameStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
272
273                 // Store the primary error string.
274                 primaryErrorString = getString(R.string.untrusted);
275                 break;
276
277             case SslError.SSL_DATE_INVALID:
278                 // Change the valid dates text view text to red.  The deprecated `getResources().getColor` must be used until the minimum API >= 23.
279                 validDatesTextView.setTextColor(getResources().getColor(R.color.red_a700));
280
281                 // Change the date span colors to red.
282                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
283                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
284
285                 // Store the primary error string.
286                 primaryErrorString = getString(R.string.invalid_date);
287                 break;
288
289             case SslError.SSL_NOTYETVALID:
290                 // Change the start date span color to red.
291                 startDateStringBuilder.setSpan(redColorSpan, startDateLabel.length(), startDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
292
293                 // Store the primary error string.
294                 primaryErrorString = getString(R.string.future_certificate);
295                 break;
296
297             case SslError.SSL_EXPIRED:
298                 // Change the end date span color to red.
299                 endDateStringBuilder.setSpan(redColorSpan, endDateLabel.length(), endDateStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
300
301                 // Store the primary error string.
302                 primaryErrorString = getString(R.string.expired_certificate);
303                 break;
304
305             case SslError.SSL_INVALID:
306                 // Store the primary error string.
307                 primaryErrorString = getString(R.string.invalid_certificate);
308                 break;
309         }
310
311
312         // Display the strings.
313         primaryErrorTextView.setText(primaryErrorString);
314         urlTextView.setText(urlStringBuilder);
315         issuedToCNameTextView.setText(issuedToCNameStringBuilder);
316         issuedToONameTextView.setText(issuedToONameStringBuilder);
317         issuedToUNameTextView.setText(issuedToUNameStringBuilder);
318         issuedByCNameTextView.setText(issuedByCNameStringBuilder);
319         issuedByONameTextView.setText(issuedByONameStringBuilder);
320         issuedByUNameTextView.setText(issuedByUNameStringBuilder);
321         startDateTextView.setText(startDateStringBuilder);
322         endDateTextView.setText(endDateStringBuilder);
323
324         // `onCreateDialog` requires the return of an alert dialog.
325         return alertDialog;
326     }
327
328
329     // This must run asynchronously because it involves a network request.  `String` declares the parameters.  `Void` does not declare progress units.  `SpannableStringBuilder` contains the results.
330     private static class GetIpAddresses extends AsyncTask<String, Void, SpannableStringBuilder> {
331         // The weak references are used to determine if the activity or the alert dialog have disappeared while the AsyncTask is running.
332         private WeakReference<Activity> activityWeakReference;
333         private WeakReference<AlertDialog> alertDialogWeakReference;
334
335         GetIpAddresses(Activity activity, AlertDialog alertDialog) {
336             // Populate the weak references.
337             activityWeakReference = new WeakReference<>(activity);
338             alertDialogWeakReference = new WeakReference<>(alertDialog);
339         }
340
341         @Override
342         protected SpannableStringBuilder doInBackground(String... domainName) {
343             // Get handles for the activity and the alert dialog.
344             Activity activity = activityWeakReference.get();
345             AlertDialog alertDialog = alertDialogWeakReference.get();
346
347             // Abort if the activity or the dialog is gone.
348             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
349                 return new SpannableStringBuilder();
350             }
351
352             // Initialize an IP address string builder.
353             StringBuilder ipAddresses = new StringBuilder();
354
355             // Get an array with the IP addresses for the host.
356             try {
357                 // Get an array with all the IP addresses for the domain.
358                 InetAddress[] inetAddressesArray = InetAddress.getAllByName(domainName[0]);
359
360                 // Add each IP address to the string builder.
361                 for (InetAddress inetAddress : inetAddressesArray) {
362                     if (ipAddresses.length() == 0) {  // This is the first IP address.
363                         // Add the IP Address to the string builder.
364                         ipAddresses.append(inetAddress.getHostAddress());
365                     } else {  // This is not the first IP address.
366                         // Add a line break to the string builder first.
367                         ipAddresses.append("\n");
368
369                         // Add the IP address to the string builder.
370                         ipAddresses.append(inetAddress.getHostAddress());
371                     }
372                 }
373             } catch (UnknownHostException exception) {
374                 // Do nothing.
375             }
376
377             // Set the label.
378             String ipAddressesLabel = activity.getString(R.string.ip_addresses) + "  ";
379
380             // Create a spannable string builder.
381             SpannableStringBuilder ipAddressesStringBuilder = new SpannableStringBuilder(ipAddressesLabel + ipAddresses);
382
383             // Get a handle for the shared preferences.
384             SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
385
386             // Get the screenshot and theme preferences.
387             boolean darkTheme = sharedPreferences.getBoolean("dark_theme", false);
388
389             // Create a blue foreground color span.
390             ForegroundColorSpan blueColorSpan;
391
392             // Set the blue color span according to the theme.  The deprecated `getColor()` must be used until the minimum API >= 23.
393             if (darkTheme) {
394                 //noinspection deprecation
395                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_400));
396             } else {
397                 //noinspection deprecation
398                 blueColorSpan = new ForegroundColorSpan(activity.getResources().getColor(R.color.blue_700));
399             }
400
401             // Set the string builder to display the certificate information in blue.  `SPAN_INCLUSIVE_INCLUSIVE` allows the span to grow in either direction.
402             ipAddressesStringBuilder.setSpan(blueColorSpan, ipAddressesLabel.length(), ipAddressesStringBuilder.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
403
404             // Return the formatted string.
405             return ipAddressesStringBuilder;
406         }
407
408         // `onPostExecute()` operates on the UI thread.
409         @Override
410         protected void onPostExecute(SpannableStringBuilder ipAddresses) {
411             // Get handles for the activity and the alert dialog.
412             Activity activity = activityWeakReference.get();
413             AlertDialog alertDialog = alertDialogWeakReference.get();
414
415             // Abort if the activity or the alert dialog is gone.
416             if ((activity == null) || (activity.isFinishing()) || (alertDialog == null)) {
417                 return;
418             }
419
420             // Get a handle for the IP addresses text view.
421             TextView ipAddressesTextView = alertDialog.findViewById(R.id.ip_addresses);
422
423             // Populate the IP addresses text view.
424             ipAddressesTextView.setText(ipAddresses);
425         }
426     }
427 }