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