Add OpenPGP encrypted export. https://redmine.stoutner.com/issues/338
[PrivacyBrowser.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ImportExportActivity.java
1 /*
2  * Copyright © 2018 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.activities;
21
22 import android.Manifest;
23 import android.app.Activity;
24 import android.app.DialogFragment;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.Environment;
31 import android.provider.DocumentsContract;
32 import android.support.annotation.NonNull;
33 import android.support.design.widget.Snackbar;
34 import android.support.design.widget.TextInputLayout;
35 import android.support.v4.app.ActivityCompat;
36 import android.support.v4.content.ContextCompat;
37 import android.support.v4.content.FileProvider;
38 import android.support.v7.app.ActionBar;
39 import android.support.v7.app.AppCompatActivity;
40 import android.support.v7.widget.CardView;
41 import android.support.v7.widget.Toolbar;
42 import android.text.Editable;
43 import android.text.TextWatcher;
44 import android.view.View;
45 import android.view.WindowManager;
46 import android.widget.AdapterView;
47 import android.widget.ArrayAdapter;
48 import android.widget.Button;
49 import android.widget.EditText;
50 import android.widget.LinearLayout;
51 import android.widget.RadioButton;
52 import android.widget.Spinner;
53 import android.widget.TextView;
54
55 import com.stoutner.privacybrowser.R;
56 import com.stoutner.privacybrowser.dialogs.ImportExportStoragePermissionDialog;
57 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
58
59 import java.io.File;
60 import java.io.FileInputStream;
61 import java.io.FileOutputStream;
62 import java.security.MessageDigest;
63 import java.security.SecureRandom;
64 import java.util.Arrays;
65
66 import javax.crypto.Cipher;
67 import javax.crypto.CipherInputStream;
68 import javax.crypto.CipherOutputStream;
69 import javax.crypto.spec.GCMParameterSpec;
70 import javax.crypto.spec.SecretKeySpec;
71
72 public class ImportExportActivity extends AppCompatActivity implements ImportExportStoragePermissionDialog.ImportExportStoragePermissionDialogListener {
73     // Create the encryption constants.
74     private final int NO_ENCRYPTION = 0;
75     private final int PASSWORD_ENCRYPTION = 1;
76     private final int OPENPGP_ENCRYPTION = 2;
77
78     // Create the activity result constants.
79     private final int BROWSE_RESULT_CODE = 0;
80     private final int OPENPGP_EXPORT_RESULT_CODE = 1;
81
82     // `openKeychainInstalled` is accessed from an inner class.
83     boolean openKeychainInstalled;
84
85     @Override
86     public void onCreate(Bundle savedInstanceState) {
87         // Disable screenshots if not allowed.
88         if (!MainWebViewActivity.allowScreenshots) {
89             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
90         }
91
92         // Set the activity theme.
93         if (MainWebViewActivity.darkTheme) {
94             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
95         } else {
96             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
97         }
98
99         // Run the default commands.
100         super.onCreate(savedInstanceState);
101
102         // Set the content view.
103         setContentView(R.layout.import_export_coordinatorlayout);
104
105         // Use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
106         Toolbar importExportAppBar = findViewById(R.id.import_export_toolbar);
107         setSupportActionBar(importExportAppBar);
108
109         // Display the home arrow on the support action bar.
110         ActionBar appBar = getSupportActionBar();
111         assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
112         appBar.setDisplayHomeAsUpEnabled(true);
113
114         // Find out if we are running KitKat
115         boolean runningKitKat = (Build.VERSION.SDK_INT == 19);
116
117         // Find out if OpenKeychain is installed.
118         try {
119             openKeychainInstalled = !getPackageManager().getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isEmpty();
120         } catch (PackageManager.NameNotFoundException exception) {
121             openKeychainInstalled = false;
122         }
123
124         // Get handles for the views that need to be modified.
125         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
126         TextInputLayout passwordEncryptionTextInputLayout = findViewById(R.id.password_encryption_textinputlayout);
127         EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
128         TextView kitKatPasswordEncryptionTextView = findViewById(R.id.kitkat_password_encryption_textview);
129         TextView openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview);
130         CardView fileLocationCardView = findViewById(R.id.file_location_cardview);
131         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
132         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
133         LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
134         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
135         TextView openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview);
136         Button importExportButton = findViewById(R.id.import_export_button);
137         TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
138
139         // Create an array adapter for the spinner.
140         ArrayAdapter<CharSequence> encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item);
141
142         // Set the drop down view resource on the spinner.
143         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items);
144
145         // Set the array adapter for the spinner.
146         encryptionSpinner.setAdapter(encryptionArrayAdapter);
147
148         // Initially hide the unneeded views.
149         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
150         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
151         openKeychainRequiredTextView.setVisibility(View.GONE);
152         fileNameLinearLayout.setVisibility(View.GONE);
153         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
154         importExportButton.setVisibility(View.GONE);
155
156         // Create strings for the default file paths.
157         String defaultFilePath;
158         String defaultPasswordEncryptionFilePath;
159
160         // Set the default file paths according to the storage permission status.
161         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
162             // Set the default file paths to use the external public directory.
163             defaultFilePath = Environment.getExternalStorageDirectory() + "/" + getString(R.string.privacy_browser_settings);
164             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
165         } else {  // The storage permission has not been granted.
166             // Set the default file paths to use the external private directory.
167             defaultFilePath = getApplicationContext().getExternalFilesDir(null) + "/" + getString(R.string.privacy_browser_settings);
168             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
169         }
170
171         // Set the default file path.
172         fileNameEditText.setText(defaultFilePath);
173
174         // Display the encryption information when the spinner changes.
175         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
176             @Override
177             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
178                 switch (position) {
179                     case NO_ENCRYPTION:
180                         // Hide the unneeded layout items.
181                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
182                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
183                         openKeychainRequiredTextView.setVisibility(View.GONE);
184                         openKeychainImportInstructionsTextView.setVisibility(View.GONE);
185
186                         // Show the file location card.
187                         fileLocationCardView.setVisibility(View.VISIBLE);
188
189                         // Show the file name linear layout if either import or export is checked.
190                         if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
191                             fileNameLinearLayout.setVisibility(View.VISIBLE);
192                         }
193
194                         // Reset the text of the import button, which may have been changed to `Decrypt`.
195                         if (importRadioButton.isChecked()) {
196                             importExportButton.setText(R.string.import_button);
197                         }
198
199                         // Reset the default file path.
200                         fileNameEditText.setText(defaultFilePath);
201
202                         // Enable the import/export button if a file name exists.
203                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
204                         break;
205
206                     case PASSWORD_ENCRYPTION:
207                         if (runningKitKat) {
208                             // Show the KitKat password encryption message.
209                             kitKatPasswordEncryptionTextView.setVisibility(View.VISIBLE);
210
211                             // Hide the OpenPGP required text view and the file location card.
212                             openKeychainRequiredTextView.setVisibility(View.GONE);
213                             fileLocationCardView.setVisibility(View.GONE);
214                         } else {
215                             // Hide the OpenPGP layout items.
216                             openKeychainRequiredTextView.setVisibility(View.GONE);
217                             openKeychainImportInstructionsTextView.setVisibility(View.GONE);
218
219                             // Show the password encryption layout items.
220                             passwordEncryptionTextInputLayout.setVisibility(View.VISIBLE);
221
222                             // Show the file location card.
223                             fileLocationCardView.setVisibility(View.VISIBLE);
224
225                             // Show the file name linear layout if either import or export is checked.
226                             if (importRadioButton.isChecked() || exportRadioButton.isChecked()) {
227                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
228                             }
229
230                             // Reset the text of the import button, which may have been changed to `Decrypt`.
231                             if (importRadioButton.isChecked()) {
232                                 importExportButton.setText(R.string.import_button);
233                             }
234
235                             // Update the default file path.
236                             fileNameEditText.setText(defaultPasswordEncryptionFilePath);
237
238                             // Enable the import/export button if a password exists.
239                             importExportButton.setEnabled(!encryptionPasswordEditText.getText().toString().isEmpty());
240                         }
241                         break;
242
243                     case OPENPGP_ENCRYPTION:
244                         // Hide the password encryption layout items.
245                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
246                         kitKatPasswordEncryptionTextView.setVisibility(View.GONE);
247
248                         // Updated items based on the installation status of OpenKeychain.
249                         if (openKeychainInstalled) {  // OpenKeychain is installed.
250                             // Remove the default file path.
251                             fileNameEditText.setText("");
252
253                             // Show the file location card.
254                             fileLocationCardView.setVisibility(View.VISIBLE);
255
256                             if (importRadioButton.isChecked()) {
257                                 // Show the file name linear layout and the OpenKeychain import instructions.
258                                 fileNameLinearLayout.setVisibility(View.VISIBLE);
259                                 openKeychainImportInstructionsTextView.setVisibility(View.VISIBLE);
260
261                                 // Set the text of the import button to be `Decrypt`.
262                                 importExportButton.setText(R.string.decrypt);
263
264                                 // Disable the import/export button.  The user needs to select a file to import first.
265                                 importExportButton.setEnabled(false);
266                             } else if (exportRadioButton.isChecked()) {
267                                 // Hide the file name linear layout and the OpenKeychain import instructions.
268                                 fileNameLinearLayout.setVisibility(View.GONE);
269                                 openKeychainImportInstructionsTextView.setVisibility(View.GONE);
270
271                                 // Enable the import/export button.
272                                 importExportButton.setEnabled(true);
273                             }
274                         } else {  // OpenKeychain is not installed.
275                             // Show the OpenPGP required layout item.
276                             openKeychainRequiredTextView.setVisibility(View.VISIBLE);
277
278                             // Hide the file location card.
279                             fileLocationCardView.setVisibility(View.GONE);
280                         }
281                         break;
282                 }
283             }
284
285             @Override
286             public void onNothingSelected(AdapterView<?> parent) {
287
288             }
289         });
290
291         // Update the status of the import/export button when the password changes.
292         encryptionPasswordEditText.addTextChangedListener(new TextWatcher() {
293             @Override
294             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
295                 // Do nothing.
296             }
297
298             @Override
299             public void onTextChanged(CharSequence s, int start, int before, int count) {
300                 // Do nothing.
301             }
302
303             @Override
304             public void afterTextChanged(Editable s) {
305                 // Enable the import/export button if a file name and password exists.
306                 importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
307             }
308         });
309
310         // Update the status of the import/export button when the file name EditText changes.
311         fileNameEditText.addTextChangedListener(new TextWatcher() {
312             @Override
313             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
314                 // Do nothing.
315             }
316
317             @Override
318             public void onTextChanged(CharSequence s, int start, int before, int count) {
319                 // Do nothing.
320             }
321
322             @Override
323             public void afterTextChanged(Editable s) {
324                 // Adjust the export button according to the encryption spinner position.
325                 switch (encryptionSpinner.getSelectedItemPosition()) {
326                     case NO_ENCRYPTION:
327                         // Enable the import/export button if a file name exists.
328                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
329                         break;
330
331                     case PASSWORD_ENCRYPTION:
332                         // Enable the import/export button if a file name and password exists.
333                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
334                         break;
335
336                     case OPENPGP_ENCRYPTION:
337                         // Enable the import/export button if OpenKeychain is installed and a file name exists.
338                         importExportButton.setEnabled(openKeychainInstalled && !fileNameEditText.getText().toString().isEmpty());
339                         break;
340                 }
341             }
342         });
343
344         // Hide the storage permissions TextView on API < 23 as permissions on older devices are automatically granted.
345         if (Build.VERSION.SDK_INT < 23) {
346             storagePermissionTextView.setVisibility(View.GONE);
347         }
348     }
349
350     public void onClickRadioButton(View view) {
351         // Get handles for the views.
352         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
353         LinearLayout fileNameLinearLayout = findViewById(R.id.file_name_linearlayout);
354         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
355         TextView openKeychainImportInstructionTextView = findViewById(R.id.openkeychain_import_instructions_textview);
356         Button importExportButton = findViewById(R.id.import_export_button);
357
358         // Check to see if import or export was selected.
359         switch (view.getId()) {
360             case R.id.import_radiobutton:
361                 // Check to see if OpenPGP encryption is selected.
362                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
363                     // Show the OpenKeychain import instructions.
364                     openKeychainImportInstructionTextView.setVisibility(View.VISIBLE);
365
366                     // Set the text on the import/export button to be `Decrypt`.
367                     importExportButton.setText(R.string.decrypt);
368
369                     // Enable the decrypt button if there is a file name.
370                     importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
371                 } else {  // OpenPGP encryption not selected.
372                     // Hide the OpenKeychain import instructions.
373                     openKeychainImportInstructionTextView.setVisibility(View.GONE);
374
375                     // Set the text on the import/export button to be `Import`.
376                     importExportButton.setText(R.string.import_button);
377                 }
378
379                 // Display the file name views.
380                 fileNameLinearLayout.setVisibility(View.VISIBLE);
381                 importExportButton.setVisibility(View.VISIBLE);
382                 break;
383
384             case R.id.export_radiobutton:
385                 // Hide the OpenKeychain import instructions.
386                 openKeychainImportInstructionTextView.setVisibility(View.GONE);
387
388                 // Set the text on the import/export button to be `Export`.
389                 importExportButton.setText(R.string.export);
390
391                 // Show the import/export button.
392                 importExportButton.setVisibility(View.VISIBLE);
393
394                 // Check to see if OpenPGP encryption is selected.
395                 if (encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
396                     // Hide the file name views.
397                     fileNameLinearLayout.setVisibility(View.GONE);
398
399                     // Enable the export button.
400                     importExportButton.setEnabled(true);
401                 } else {  // OpenPGP encryption is not selected.
402                     // Show the file name views.
403                     fileNameLinearLayout.setVisibility(View.VISIBLE);
404                 }
405                 break;
406         }
407     }
408
409     public void browse(View view) {
410         // Get a handle for the import radiobutton.
411         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
412
413         // Check to see if import or export is selected.
414         if (importRadioButton.isChecked()) {  // Import is selected.
415             // Create the file picker intent.
416             Intent importBrowseIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
417
418             // Set the intent MIME type to include all files.
419             importBrowseIntent.setType("*/*");
420
421             // Set the initial directory if API >= 26.
422             if (Build.VERSION.SDK_INT >= 26) {
423                 importBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
424             }
425
426             // Specify that a file that can be opened is requested.
427             importBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
428
429             // Launch the file picker.
430             startActivityForResult(importBrowseIntent, BROWSE_RESULT_CODE);
431         } else {  // Export is selected
432             // Create the file picker intent.
433             Intent exportBrowseIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
434
435             // Set the intent MIME type to include all files.
436             exportBrowseIntent.setType("*/*");
437
438             // Set the initial export file name.
439             exportBrowseIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.privacy_browser_settings));
440
441             // Set the initial directory if API >= 26.
442             if (Build.VERSION.SDK_INT >= 26) {
443                 exportBrowseIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
444             }
445
446             // Specify that a file that can be opened is requested.
447             exportBrowseIntent.addCategory(Intent.CATEGORY_OPENABLE);
448
449             // Launch the file picker.
450             startActivityForResult(exportBrowseIntent, BROWSE_RESULT_CODE);
451         }
452     }
453
454     public void importExport(View view) {
455         // Get a handle for the views.
456         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
457         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
458         RadioButton exportRadioButton = findViewById(R.id.export_radiobutton);
459
460         // Check to see if the storage permission is needed.
461         if ((encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION) && exportRadioButton.isChecked()) {  // Permission not needed to export via OpenKeychain.
462             // Export the settings.
463             exportSettings();
464         } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
465             // Check to see if import or export is selected.
466             if (importRadioButton.isChecked()) {  // Import is selected.
467                 // Import the settings.
468                 importSettings();
469             } else {  // Export is selected.
470                 // Export the settings.
471                 exportSettings();
472             }
473         } else {  // The storage permission has not been granted.
474             // Get a handle for the file name EditText.
475             EditText fileNameEditText = findViewById(R.id.file_name_edittext);
476
477             // Get the file name string.
478             String fileNameString = fileNameEditText.getText().toString();
479
480             // Get the external private directory `File`.
481             File externalPrivateDirectoryFile = getApplicationContext().getExternalFilesDir(null);
482
483             // Remove the lint error below that the `File` might be null.
484             assert externalPrivateDirectoryFile != null;
485
486             // Get the external private directory string.
487             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
488
489             // Check to see if the file path is in the external private directory.
490             if (fileNameString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
491                 // Check to see if import or export is selected.
492                 if (importRadioButton.isChecked()) {  // Import is selected.
493                     // Import the settings.
494                     importSettings();
495                 } else {  // Export is selected.
496                     // Export the settings.
497                     exportSettings();
498                 }
499             } else {  // The file path is in a public directory.
500                 // Check if the user has previously denied the storage permission.
501                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
502                     // Instantiate the storage permission alert dialog.
503                     DialogFragment importExportStoragePermissionDialogFragment = new ImportExportStoragePermissionDialog();
504
505                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
506                     importExportStoragePermissionDialogFragment.show(getFragmentManager(), getString(R.string.storage_permission));
507                 } else {  // Show the permission request directly.
508                     // Request the storage permission.  The export will be run when it finishes.
509                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
510                 }
511             }
512         }
513     }
514
515     @Override
516     public void onCloseImportExportStoragePermissionDialog() {
517         // Request the write external storage permission.  The import/export will be run when it finishes.
518         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
519     }
520
521     @Override
522     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
523         // Get a handle for the import radiobutton.
524         RadioButton importRadioButton = findViewById(R.id.import_radiobutton);
525
526         // Check to see if import or export is selected.
527         if (importRadioButton.isChecked()) {  // Import is selected.
528             // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
529             if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
530                 // Import the settings.
531                 importSettings();
532             } else {  // The storage permission was not granted.
533                 // Display an error snackbar.
534                 Snackbar.make(importRadioButton, getString(R.string.cannot_import), Snackbar.LENGTH_LONG).show();
535             }
536         } else {  // Export is selected.
537             // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
538             if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
539                 // Export the settings.
540                 exportSettings();
541             } else {  // The storage permission was not granted.
542                 // Display an error snackbar.
543                 Snackbar.make(importRadioButton, getString(R.string.cannot_export), Snackbar.LENGTH_LONG).show();
544             }
545         }
546     }
547
548     @Override
549     public void onActivityResult(int requestCode, int resultCode, Intent data) {
550         switch (requestCode) {
551             case (BROWSE_RESULT_CODE):
552                 // Don't do anything if the user pressed back from the file picker.
553                 if (resultCode == Activity.RESULT_OK) {
554                     // Get a handle for the file name EditText.
555                     EditText fileNameEditText = findViewById(R.id.file_name_edittext);
556
557                     // Get the file name URI.
558                     Uri fileNameUri = data.getData();
559
560                     // Remove the lint warning that the file name URI might be null.
561                     assert fileNameUri != null;
562
563                     // Get the raw file name path.
564                     String rawFileNamePath = fileNameUri.getPath();
565
566                     // Remove the warning that the file name path might be null.
567                     assert rawFileNamePath != null;
568
569                     // Check to see if the file name Path includes a valid storage location.
570                     if (rawFileNamePath.contains(":")) {  // The path is valid.
571                         // Split the path into the initial content uri and the final path information.
572                         String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
573                         String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
574
575                         // Create the file name path string.
576                         String fileNamePath;
577
578                         // Construct the file name path.
579                         switch (fileNameContentPath) {
580                             // The documents home has a special content path.
581                             case "/document/home":
582                                 fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
583                                 break;
584
585                             // Everything else for the primary user should be in `/document/primary`.
586                             case "/document/primary":
587                                 fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
588                                 break;
589
590                             // Just in case, catch everything else and place it in the external storage directory.
591                             default:
592                                 fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
593                                 break;
594                         }
595
596                         // Set the file name path as the text of the file name EditText.
597                         fileNameEditText.setText(fileNamePath);
598                     } else {  // The path is invalid.
599                         Snackbar.make(fileNameEditText, rawFileNamePath + " " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
600                     }
601                 }
602                 break;
603
604             case OPENPGP_EXPORT_RESULT_CODE:
605                 // Get the temporary unencrypted export file.
606                 File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.privacy_browser_settings));
607
608                 // Delete the temporary unencrypted export file if it exists.
609                 if (temporaryUnencryptedExportFile.exists()) {
610                     //noinspection ResultOfMethodCallIgnored
611                     temporaryUnencryptedExportFile.delete();
612                 }
613                 break;
614         }
615     }
616
617     private void exportSettings() {
618         // Get a handle for the views.
619         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
620         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
621
622         // Instantiate the import export database helper.
623         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
624
625         // Get the export and temporary unencrypted export files.
626         File exportFile = new File(fileNameEditText.getText().toString());
627         File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.privacy_browser_settings));
628
629         // Initialize the export status string.
630         String exportStatus;
631
632         // Export according to the encryption type.
633         switch (encryptionSpinner.getSelectedItemPosition()) {
634             case NO_ENCRYPTION:
635                 // Export the unencrypted file.
636                 exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, this);
637
638                 // Show a disposition snackbar.
639                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
640                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
641                 } else {
642                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
643                 }
644                 break;
645
646             case PASSWORD_ENCRYPTION:
647                 // Create an unencrypted export in a private directory.
648                 exportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
649
650                 try {
651                     // Create an unencrypted export file input stream.
652                     FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
653
654                     // Delete the encrypted export file if it exists.
655                     if (exportFile.exists()) {
656                         //noinspection ResultOfMethodCallIgnored
657                         exportFile.delete();
658                     }
659
660                     // Create an encrypted export file output stream.
661                     FileOutputStream encryptedExportFileOutputStream = new FileOutputStream(exportFile);
662
663                     // Get a handle for the encryption password EditText.
664                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
665
666                     // Get the encryption password.
667                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
668
669                     // Initialize a secure random number generator.
670                     SecureRandom secureRandom = new SecureRandom();
671
672                     // Get a 256 bit (32 byte) random salt.
673                     byte[] saltByteArray = new byte[32];
674                     secureRandom.nextBytes(saltByteArray);
675
676                     // Convert the encryption password to a byte array.
677                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes("UTF-8");
678
679                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
680                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
681                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
682                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
683
684                     // Get a SHA-512 message digest.
685                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
686
687                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
688                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
689
690                     // Truncate the encryption password byte array to 256 bits (32 bytes).
691                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
692
693                     // Create an AES secret key from the encryption password byte array.
694                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
695
696                     // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
697                     byte[] initializationVector = new byte[12];
698                     secureRandom.nextBytes(initializationVector);
699
700                     // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
701                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
702
703                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
704                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
705
706                     // Initialize the cipher.
707                     cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
708
709                     // Add the salt and the initialization vector to the export file.
710                     encryptedExportFileOutputStream.write(saltByteArray);
711                     encryptedExportFileOutputStream.write(initializationVector);
712
713                     // Create a cipher output stream.
714                     CipherOutputStream cipherOutputStream = new CipherOutputStream(encryptedExportFileOutputStream, cipher);
715
716                     // Initialize variables to store data as it is moved from the unencrypted export file input stream to the cipher output stream.  Move 128 bits (16 bytes) at a time.
717                     int numberOfBytesRead;
718                     byte[] encryptedBytes = new byte[16];
719
720                     // Read up to 128 bits (16 bytes) of data from the unencrypted export file stream.  `-1` will be returned when the end of the file is reached.
721                     while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
722                         // Write the data to the cipher output stream.
723                         cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
724                     }
725
726                     // Close the streams.
727                     cipherOutputStream.flush();
728                     cipherOutputStream.close();
729                     encryptedExportFileOutputStream.close();
730                     unencryptedExportFileInputStream.close();
731
732                     // Wipe the encryption data from memory.
733                     //noinspection UnusedAssignment
734                     encryptionPasswordString = "";
735                     Arrays.fill(saltByteArray, (byte) 0);
736                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
737                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
738                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
739                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
740                     Arrays.fill(initializationVector, (byte) 0);
741                     Arrays.fill(encryptedBytes, (byte) 0);
742
743                     // Delete the temporary unencrypted export file.
744                     //noinspection ResultOfMethodCallIgnored
745                     temporaryUnencryptedExportFile.delete();
746                 } catch (Exception exception) {
747                     exportStatus = exception.toString();
748                 }
749
750                 // Show a disposition snackbar.
751                 if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
752                     Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
753                 } else {
754                     Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
755                 }
756                 break;
757
758             case OPENPGP_ENCRYPTION:
759                 // Create an unencrypted export in the private location.
760                 importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
761
762                 // Create an encryption intent for OpenKeychain.
763                 Intent openKeychainEncryptIntent = new Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA");
764
765                 // Include the temporary unencrypted export file URI.
766                 openKeychainEncryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryUnencryptedExportFile));
767
768                 // Allow OpenKeychain to read the file URI.
769                 openKeychainEncryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
770
771                 // Send the intent to the OpenKeychain package.
772                 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain");
773
774                 // Make it so.
775                 startActivityForResult(openKeychainEncryptIntent, OPENPGP_EXPORT_RESULT_CODE);
776                 break;
777         }
778     }
779
780     private void importSettings() {
781         // Get a handle for the views.
782         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
783         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
784
785         // Instantiate the import export database helper.
786         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
787
788         // Get the import file.
789         File importFile = new File(fileNameEditText.getText().toString());
790
791         // Initialize the import status string
792         String importStatus = "";
793
794         // Import according to the encryption type.
795         switch (encryptionSpinner.getSelectedItemPosition()) {
796             case NO_ENCRYPTION:
797                 // Import the unencrypted file.
798                 importStatus = importExportDatabaseHelper.importUnencrypted(importFile, this);
799                 break;
800
801             case PASSWORD_ENCRYPTION:
802                 // Use a private temporary import location.
803                 File temporaryUnencryptedImportFile = new File(getApplicationContext().getCacheDir() + "/" + getString(R.string.privacy_browser_settings));
804
805                 try {
806                     // Create an encrypted import file input stream.
807                     FileInputStream encryptedImportFileInputStream = new FileInputStream(importFile);
808
809                     // Delete the temporary import file if it exists.
810                     if (temporaryUnencryptedImportFile.exists()) {
811                         //noinspection ResultOfMethodCallIgnored
812                         temporaryUnencryptedImportFile.delete();
813                     }
814
815                     // Create an unencrypted import file output stream.
816                     FileOutputStream unencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
817
818                     // Get a handle for the encryption password EditText.
819                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
820
821                     // Get the encryption password.
822                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
823
824                     // Get the salt from the beginning of the import file.
825                     byte[] saltByteArray = new byte[32];
826                     //noinspection ResultOfMethodCallIgnored
827                     encryptedImportFileInputStream.read(saltByteArray);
828
829                     // Get the initialization vector from the import file.
830                     byte[] initializationVector = new byte[12];
831                     //noinspection ResultOfMethodCallIgnored
832                     encryptedImportFileInputStream.read(initializationVector);
833
834                     // Convert the encryption password to a byte array.
835                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes("UTF-8");
836
837                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
838                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
839                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
840                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
841
842                     // Get a SHA-512 message digest.
843                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
844
845                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
846                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
847
848                     // Truncate the encryption password byte array to 256 bits (32 bytes).
849                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
850
851                     // Create an AES secret key from the encryption password byte array.
852                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
853
854                     // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext.  It doesn't use padding.
855                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
856
857                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
858                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
859
860                     // Initialize the cipher.
861                     cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
862
863                     // Create a cipher input stream.
864                     CipherInputStream cipherInputStream = new CipherInputStream(encryptedImportFileInputStream, cipher);
865
866                     // Initialize variables to store data as it is moved from the cipher input stream to the unencrypted import file output stream.  Move 128 bits (16 bytes) at a time.
867                     int numberOfBytesRead;
868                     byte[] decryptedBytes = new byte[16];
869
870                     // Read up to 128 bits (16 bytes) of data from the cipher input stream.  `-1` will be returned when the end fo the file is reached.
871                     while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
872                         // Write the data to the unencrypted import file output stream.
873                         unencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
874                     }
875
876                     // Close the streams.
877                     unencryptedImportFileOutputStream.flush();
878                     unencryptedImportFileOutputStream.close();
879                     cipherInputStream.close();
880                     encryptedImportFileInputStream.close();
881
882                     // Wipe the encryption data from memory.
883                     //noinspection UnusedAssignment
884                     encryptionPasswordString = "";
885                     Arrays.fill(saltByteArray, (byte) 0);
886                     Arrays.fill(initializationVector, (byte) 0);
887                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
888                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
889                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
890                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
891                     Arrays.fill(decryptedBytes, (byte) 0);
892
893                     // Import the unencrypted database from the private location.
894                     importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFile, this);
895
896                     // Delete the temporary unencrypted import file.
897                     //noinspection ResultOfMethodCallIgnored
898                     temporaryUnencryptedImportFile.delete();
899                 } catch (Exception exception) {
900                     importStatus = exception.toString();
901                 }
902                 break;
903
904             case OPENPGP_ENCRYPTION:
905                 try {
906                     // Create an decryption intent for OpenKeychain.
907                     Intent openKeychainDecryptIntent = new Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA");
908
909                     // Include the URI to be decrypted.
910                     openKeychainDecryptIntent.setData(FileProvider.getUriForFile(this, getString(R.string.file_provider), importFile));
911
912                     // Allow OpenKeychain to read the file URI.
913                     openKeychainDecryptIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
914
915                     // Send the intent to the OpenKeychain package.
916                     openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain");
917
918                     // Make it so.
919                     startActivity(openKeychainDecryptIntent);
920                 } catch (IllegalArgumentException exception) {  // The file import location is not valid.
921                     // Display a snack bar with the import error.
922                     Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + exception.toString(), Snackbar.LENGTH_INDEFINITE).show();
923                 }
924                 break;
925         }
926
927         // Respond to the import disposition.
928         if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
929             // Create an intent to restart Privacy Browser.
930             Intent restartIntent = getParentActivityIntent();
931
932             // Assert that the intent is not null to remove the lint error below.
933             assert restartIntent != null;
934
935             // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
936             restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
937
938             // Make it so.
939             startActivity(restartIntent);
940         } else if (!(encryptionSpinner.getSelectedItemPosition() == OPENPGP_ENCRYPTION)){  // The import was not successful.
941             // Display a snack bar with the import error.
942             Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
943         }
944     }
945 }