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