]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.java
14f5789fe3a9b7fbfe3a8d0fc59f42bd8be71ba0
[PrivacyBrowserAndroid.git] / app / src / main / java / com / stoutner / privacybrowser / activities / ImportExportActivity.java
1 /*
2  * Copyright © 2018 Soren Stoutner <soren@stoutner.com>.
3  *
4  * This file is part of Privacy Browser <https://www.stoutner.com/privacy-browser>.
5  *
6  * Privacy Browser is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Privacy Browser is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Privacy Browser.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 package com.stoutner.privacybrowser.activities;
21
22 import android.Manifest;
23 import android.app.Activity;
24 import android.app.DialogFragment;
25 import android.content.Intent;
26 import android.content.pm.PackageManager;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.os.Bundle;
30 import android.os.Environment;
31 import android.provider.DocumentsContract;
32 import android.support.annotation.NonNull;
33 import android.support.design.widget.Snackbar;
34 import android.support.design.widget.TextInputLayout;
35 import android.support.v4.app.ActivityCompat;
36 import android.support.v4.content.ContextCompat;
37 import android.support.v7.app.ActionBar;
38 import android.support.v7.app.AppCompatActivity;
39 import android.support.v7.widget.Toolbar;
40 import android.text.Editable;
41 import android.text.TextWatcher;
42 import android.view.View;
43 import android.view.WindowManager;
44 import android.widget.AdapterView;
45 import android.widget.ArrayAdapter;
46 import android.widget.Button;
47 import android.widget.EditText;
48 import android.widget.Spinner;
49 import android.widget.TextView;
50
51 import com.stoutner.privacybrowser.R;
52 import com.stoutner.privacybrowser.dialogs.ImportExportStoragePermissionDialog;
53 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper;
54
55 import java.io.File;
56 import java.io.FileInputStream;
57 import java.io.FileOutputStream;
58 import java.security.MessageDigest;
59 import java.security.SecureRandom;
60 import java.util.Arrays;
61
62 import javax.crypto.Cipher;
63 import javax.crypto.CipherInputStream;
64 import javax.crypto.CipherOutputStream;
65 import javax.crypto.spec.GCMParameterSpec;
66 import javax.crypto.spec.SecretKeySpec;
67
68 public class ImportExportActivity extends AppCompatActivity implements ImportExportStoragePermissionDialog.ImportExportStoragePermissionDialogListener {
69     // Create the encryption constants.
70     private final int NO_ENCRYPTION = 0;
71     private final int PASSWORD_ENCRYPTION = 1;
72     private final int GPG_ENCRYPTION = 2;
73
74     // Create the action constants.
75     private final int IMPORT = 0;
76     private final int EXPORT = 1;
77
78     @Override
79     public void onCreate(Bundle savedInstanceState) {
80         // Disable screenshots if not allowed.
81         if (!MainWebViewActivity.allowScreenshots) {
82             getWindow().addFlags(WindowManager.LayoutParams.FLAG_SECURE);
83         }
84
85         // Set the activity theme.
86         if (MainWebViewActivity.darkTheme) {
87             setTheme(R.style.PrivacyBrowserDark_SecondaryActivity);
88         } else {
89             setTheme(R.style.PrivacyBrowserLight_SecondaryActivity);
90         }
91
92         // Run the default commands.
93         super.onCreate(savedInstanceState);
94
95         // Set the content view.
96         setContentView(R.layout.import_export_coordinatorlayout);
97
98         // Use the `SupportActionBar` from `android.support.v7.app.ActionBar` until the minimum API is >= 21.
99         Toolbar importExportAppBar = findViewById(R.id.import_export_toolbar);
100         setSupportActionBar(importExportAppBar);
101
102         // Display the home arrow on the support action bar.
103         ActionBar appBar = getSupportActionBar();
104         assert appBar != null;// This assert removes the incorrect warning in Android Studio on the following line that `appBar` might be null.
105         appBar.setDisplayHomeAsUpEnabled(true);
106
107         // Get handles for the views that need to be modified.
108         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
109         TextInputLayout passwordEncryptionTextInputLayout = findViewById(R.id.password_encryption_textinputlayout);
110         EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
111         Spinner importExportSpinner = findViewById(R.id.import_export_spinner);
112         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
113         Button importExportButton = findViewById(R.id.import_export_button);
114         TextView storagePermissionTextView = findViewById(R.id.import_export_storage_permission_textview);
115
116         // Create array adapters for the spinners.
117         ArrayAdapter<CharSequence> encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item);
118         ArrayAdapter<CharSequence> importExportArrayAdapter = ArrayAdapter.createFromResource(this, R.array.import_export_spinner, R.layout.spinner_item);
119
120         // Set the drop down view resource on the spinners.
121         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items);
122         importExportArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items);
123
124         // Set the array adapters for the spinners.
125         encryptionSpinner.setAdapter(encryptionArrayAdapter);
126         importExportSpinner.setAdapter(importExportArrayAdapter);
127
128         // Initially hide the encryption layout items.
129         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
130
131         // Create strings for the default file paths.
132         String defaultFilePath;
133         String defaultPasswordEncryptionFilePath;
134         String defaultGpgEncryptionFilePath;
135
136         // Set the default file paths according to the storage permission status.
137         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // The storage permission has been granted.
138             // Set the default file paths to use the external public directory.
139             defaultFilePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + getString(R.string.privacy_browser_settings);
140             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
141             defaultGpgEncryptionFilePath = defaultFilePath + ".gpg";
142         } else {  // The storage permission has not been granted.
143             // Set the default file paths to use the external private directory.
144             defaultFilePath = getApplicationContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS) + "/" + getString(R.string.privacy_browser_settings);
145             defaultPasswordEncryptionFilePath = defaultFilePath + ".aes";
146             defaultGpgEncryptionFilePath = defaultFilePath + ".gpg";
147         }
148
149         // Set the default file path.
150         fileNameEditText.setText(defaultFilePath);
151
152         // Display the encryption information when the spinner changes.
153         encryptionSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
154             @Override
155             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
156                 switch (position) {
157                     case NO_ENCRYPTION:
158                         // Hide the encryption layout items.
159                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
160
161                         // Reset the default file path.
162                         fileNameEditText.setText(defaultFilePath);
163
164                         // Enable the import/export button if a file name exists.
165                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
166                         break;
167
168                     case PASSWORD_ENCRYPTION:
169                         // Show the password encryption layout items.
170                         passwordEncryptionTextInputLayout.setVisibility(View.VISIBLE);
171
172                         // Update the default file path.
173                         fileNameEditText.setText(defaultPasswordEncryptionFilePath);
174
175                         // Enable the import/export button if a file name and password exists.
176                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
177                         break;
178
179                     case GPG_ENCRYPTION:
180                         // Hide the password encryption layout items.
181                         passwordEncryptionTextInputLayout.setVisibility(View.GONE);
182
183                         // Update the default file path.
184                         fileNameEditText.setText(defaultGpgEncryptionFilePath);
185                         break;
186                 }
187             }
188
189             @Override
190             public void onNothingSelected(AdapterView<?> parent) {
191
192             }
193         });
194
195         // Update the import/export button when the spinner changes.
196         importExportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
197             @Override
198             public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
199                 switch (position) {
200                     case IMPORT:
201                         importExportButton.setText(R.string.import_button);
202                         break;
203
204                     case EXPORT:
205                         importExportButton.setText(R.string.export);
206                         break;
207                 }
208             }
209
210             @Override
211             public void onNothingSelected(AdapterView<?> parent) {
212
213             }
214         });
215
216         // Update the status of the import/export button when the password changes.
217         encryptionPasswordEditText.addTextChangedListener(new TextWatcher() {
218             @Override
219             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
220                 // Do nothing.
221             }
222
223             @Override
224             public void onTextChanged(CharSequence s, int start, int before, int count) {
225                 // Do nothing.
226             }
227
228             @Override
229             public void afterTextChanged(Editable s) {
230                 // Enable the import/export button if a file name and password exists.
231                 importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
232             }
233         });
234
235         // Update the status of the import/export button when the file name EditText changes.
236         fileNameEditText.addTextChangedListener(new TextWatcher() {
237             @Override
238             public void beforeTextChanged(CharSequence s, int start, int count, int after) {
239                 // Do nothing.
240             }
241
242             @Override
243             public void onTextChanged(CharSequence s, int start, int before, int count) {
244                 // Do nothing.
245             }
246
247             @Override
248             public void afterTextChanged(Editable s) {
249                 // Adjust the export button according to the encryption spinner position.
250                 switch (encryptionSpinner.getSelectedItemPosition()) {
251                     case NO_ENCRYPTION:
252                         // Enable the import/export button if a file name exists.
253                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty());
254                         break;
255
256                     case PASSWORD_ENCRYPTION:
257                         // Enable the import/export button if a file name and password exists.
258                         importExportButton.setEnabled(!fileNameEditText.getText().toString().isEmpty() && !encryptionPasswordEditText.getText().toString().isEmpty());
259                         break;
260
261                     case GPG_ENCRYPTION:
262                         break;
263                 }
264             }
265         });
266
267         // Hide the storage permissions TextView on API < 23 as permissions on older devices are automatically granted.
268         if (Build.VERSION.SDK_INT < 23) {
269             storagePermissionTextView.setVisibility(View.GONE);
270         }
271     }
272
273     public void browse(View view) {
274         // Get a handle for the import/export spinner.
275         Spinner importExportSpinner = findViewById(R.id.import_export_spinner);
276
277         // Check to see if import or export is selected.
278         if (importExportSpinner.getSelectedItemPosition() == IMPORT) {  // Import is selected.
279             // Create the file picker intent.
280             Intent importIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
281
282             // Set the intent MIME type to include all files.
283             importIntent.setType("*/*");
284
285             // Set the initial directory if API >= 26.
286             if (Build.VERSION.SDK_INT >= 26) {
287                 importIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
288             }
289
290             // Specify that a file that can be opened is requested.
291             importIntent.addCategory(Intent.CATEGORY_OPENABLE);
292
293             // Launch the file picker.
294             startActivityForResult(importIntent, 0);
295         } else {  // Export is selected
296             // Create the file picker intent.
297             Intent exportIntent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
298
299             // Set the intent MIME type to include all files.
300             exportIntent.setType("*/*");
301
302             // Set the initial export file name.
303             exportIntent.putExtra(Intent.EXTRA_TITLE, getString(R.string.privacy_browser_settings));
304
305             // Set the initial directory if API >= 26.
306             if (Build.VERSION.SDK_INT >= 26) {
307                 exportIntent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Environment.getExternalStorageDirectory());
308             }
309
310             // Specify that a file that can be opened is requested.
311             exportIntent.addCategory(Intent.CATEGORY_OPENABLE);
312
313             // Launch the file picker.
314             startActivityForResult(exportIntent, 0);
315         }
316     }
317
318     public void importExport(View view) {
319         // Get a handle for the import/export spinner.
320         Spinner importExportSpinner = findViewById(R.id.import_export_spinner);
321
322         // Check to see if the storage permission has been granted.
323         if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {  // Storage permission granted.
324             // Check to see if import or export is selected.
325             if (importExportSpinner.getSelectedItemPosition() == IMPORT) {  // Import is selected.
326                 // Import the settings.
327                 importSettings();
328             } else {  // Export is selected.
329                 // Export the settings.
330                 exportSettings();
331             }
332         } else {  // Storage permission not granted.
333             // Get a handle for the file name EditText.
334             EditText fileNameEditText = findViewById(R.id.file_name_edittext);
335
336             // Get the file name string.
337             String fileNameString = fileNameEditText.getText().toString();
338
339             // Get the external private directory `File`.
340             File externalPrivateDirectoryFile = getApplicationContext().getExternalFilesDir(null);
341
342             // Remove the lint error below that the `File` might be null.
343             assert externalPrivateDirectoryFile != null;
344
345             // Get the external private directory string.
346             String externalPrivateDirectory = externalPrivateDirectoryFile.toString();
347
348             // Check to see if the file path is in the external private directory.
349             if (fileNameString.startsWith(externalPrivateDirectory)) {  // The file path is in the external private directory.
350                 // Check to see if import or export is selected.
351                 if (importExportSpinner.getSelectedItemPosition() == IMPORT) {  // Import is selected.
352                     // Import the settings.
353                     importSettings();
354                 } else {  // Export is selected.
355                     // Export the settings.
356                     exportSettings();
357                 }
358             } else {  // The file path is in a public directory.
359                 // Check if the user has previously denied the storage permission.
360                 if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {  // Show a dialog explaining the request first.
361                     // Instantiate the storage permission alert dialog.
362                     DialogFragment importExportStoragePermissionDialogFragment = new ImportExportStoragePermissionDialog();
363
364                     // Show the storage permission alert dialog.  The permission will be requested when the dialog is closed.
365                     importExportStoragePermissionDialogFragment.show(getFragmentManager(), getString(R.string.storage_permission));
366                 } else {  // Show the permission request directly.
367                     // Request the storage permission.  The export will be run when it finishes.
368                     ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
369                 }
370             }
371         }
372     }
373
374     @Override
375     public void onCloseImportExportStoragePermissionDialog() {
376         // Request the write external storage permission.  The import/export will be run when it finishes.
377         ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
378     }
379
380     @Override
381     public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
382         // Get a handle for the import/export spinner.
383         Spinner importExportSpinner = findViewById(R.id.import_export_spinner);
384
385         // Check to see if import or export is selected.
386         if (importExportSpinner.getSelectedItemPosition() == IMPORT) {  // Import is selected.
387             // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
388             if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
389                 // Import the settings.
390                 importSettings();
391             } else {  // The storage permission was not granted.
392                 // Display an error snackbar.
393                 Snackbar.make(importExportSpinner, getString(R.string.cannot_import), Snackbar.LENGTH_LONG).show();
394             }
395         } else {  // Export is selected.
396             // Check to see if the storage permission was granted.  If the dialog was canceled the grant results will be empty.
397             if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {  // The storage permission was granted.
398                 // Export the settings.
399                 exportSettings();
400             } else {  // The storage permission was not granted.
401                 // Display an error snackbar.
402                 Snackbar.make(importExportSpinner, getString(R.string.cannot_export), Snackbar.LENGTH_LONG).show();
403             }
404         }
405     }
406
407     @Override
408     public void onActivityResult(int requestCode, int resultCode, Intent data) {
409         // Don't do anything if the user pressed back from the file picker.
410         if (resultCode == Activity.RESULT_OK) {
411             // Get a handle for the file name EditText.
412             EditText fileNameEditText = findViewById(R.id.file_name_edittext);
413
414             // Get the file name URI.
415             Uri fileNameUri = data.getData();
416
417             // Remove the lint warning that the file name URI might be null.
418             assert fileNameUri != null;
419
420             // Get the raw file name path.
421             String rawFileNamePath = fileNameUri.getPath();
422
423             // Remove the warning that the file name path might be null.
424             assert rawFileNamePath != null;
425
426             // Check to see if the file name Path includes a valid storage location.
427             if (rawFileNamePath.contains(":")) {  // The path is valid.
428                 // Split the path into the initial content uri and the final path information.
429                 String fileNameContentPath = rawFileNamePath.substring(0, rawFileNamePath.indexOf(":"));
430                 String fileNameFinalPath = rawFileNamePath.substring(rawFileNamePath.indexOf(":") + 1);
431
432                 // Create the file name path string.
433                 String fileNamePath;
434
435                 // Construct the file name path.
436                 switch (fileNameContentPath) {
437                     // The documents home has a special content path.
438                     case "/document/home":
439                         fileNamePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS) + "/" + fileNameFinalPath;
440                         break;
441
442                     // Everything else for the primary user should be in `/document/primary`.
443                     case "/document/primary":
444                         fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
445                         break;
446
447                     // Just in case, catch everything else and place it in the external storage directory.
448                     default:
449                         fileNamePath = Environment.getExternalStorageDirectory() + "/" + fileNameFinalPath;
450                         break;
451                 }
452
453                 // Set the file name path as the text of the file name EditText.
454                 fileNameEditText.setText(fileNamePath);
455             } else {  // The path is invalid.
456                 Snackbar.make(fileNameEditText, rawFileNamePath + " + " + getString(R.string.invalid_location), Snackbar.LENGTH_INDEFINITE).show();
457             }
458         }
459     }
460
461     private void exportSettings() {
462         // Get a handle for the views.
463         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
464         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
465
466         // Instantiate the import export database helper.
467         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
468
469         // Get the export file.
470         File exportFile = new File(fileNameEditText.getText().toString());
471
472         // Initialize the export status string.
473         String exportStatus = "";
474
475         // Export according to the encryption type.
476         switch (encryptionSpinner.getSelectedItemPosition()) {
477             case NO_ENCRYPTION:
478                 // Export the unencrypted file.
479                 exportStatus = importExportDatabaseHelper.exportUnencrypted(exportFile, this);
480                 break;
481
482             case PASSWORD_ENCRYPTION:
483                 // Use a private temporary export location.
484                 File temporaryUnencryptedExportFile = new File(getApplicationContext().getCacheDir() + "/export.temp");
485
486                 // Create an unencrypted export in the private location.
487                 exportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportFile, this);
488
489                 try {
490                     // Create an unencrypted export file input stream.
491                     FileInputStream unencryptedExportFileInputStream = new FileInputStream(temporaryUnencryptedExportFile);
492
493                     // Delete the encrypted export file if it exists.
494                     if (exportFile.exists()) {
495                         //noinspection ResultOfMethodCallIgnored
496                         exportFile.delete();
497                     }
498
499                     // Create an encrypted export file output stream.
500                     FileOutputStream encryptedExportFileOutputStream = new FileOutputStream(exportFile);
501
502                     // Get a handle for the encryption password EditText.
503                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
504
505                     // Get the encryption password.
506                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
507
508                     // Initialize a secure random number generator.
509                     SecureRandom secureRandom = new SecureRandom();
510
511                     // Get a 256 bit (32 byte) random salt.
512                     byte[] saltByteArray = new byte[32];
513                     secureRandom.nextBytes(saltByteArray);
514
515                     // Convert the encryption password to a byte array.
516                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes("UTF-8");
517
518                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
519                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
520                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
521                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
522
523                     // Get a SHA-512 message digest.
524                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
525
526                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
527                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
528
529                     // Truncate the encryption password byte array to 256 bits (32 bytes).
530                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
531
532                     // Create an AES secret key from the encryption password byte array.
533                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
534
535                     // Generate a random 12 byte initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
536                     byte[] initializationVector = new byte[12];
537                     secureRandom.nextBytes(initializationVector);
538
539                     // 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.
540                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
541
542                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
543                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
544
545                     // Initialize the cipher.
546                     cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
547
548                     // Add the salt and the initialization vector to the export file.
549                     encryptedExportFileOutputStream.write(saltByteArray);
550                     encryptedExportFileOutputStream.write(initializationVector);
551
552                     // Create a cipher output stream.
553                     CipherOutputStream cipherOutputStream = new CipherOutputStream(encryptedExportFileOutputStream, cipher);
554
555                     // 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.
556                     int numberOfBytesRead;
557                     byte[] encryptedBytes = new byte[16];
558
559                     // 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.
560                     while ((numberOfBytesRead = unencryptedExportFileInputStream.read(encryptedBytes)) != -1) {
561                         // Write the data to the cipher output stream.
562                         cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead);
563                     }
564
565                     // Close the streams.
566                     cipherOutputStream.flush();
567                     cipherOutputStream.close();
568                     encryptedExportFileOutputStream.close();
569                     unencryptedExportFileInputStream.close();
570
571                     // Wipe the encryption data from memory.
572                     //noinspection UnusedAssignment
573                     encryptionPasswordString = "";
574                     Arrays.fill(saltByteArray, (byte) 0);
575                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
576                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
577                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
578                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
579                     Arrays.fill(initializationVector, (byte) 0);
580                     Arrays.fill(encryptedBytes, (byte) 0);
581
582                     // Delete the temporary unencrypted export file.
583                     //noinspection ResultOfMethodCallIgnored
584                     temporaryUnencryptedExportFile.delete();
585                 } catch (Exception exception) {
586                     exportStatus = exception.toString();
587                 }
588                 break;
589
590             case GPG_ENCRYPTION:
591
592                 break;
593         }
594
595         // Show a disposition snackbar.
596         if (exportStatus.equals(ImportExportDatabaseHelper.EXPORT_SUCCESSFUL)) {
597             Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show();
598         } else {
599             Snackbar.make(fileNameEditText, getString(R.string.export_failed) + "  " + exportStatus, Snackbar.LENGTH_INDEFINITE).show();
600         }
601     }
602
603     private void importSettings() {
604         // Get a handle for the views.
605         Spinner encryptionSpinner = findViewById(R.id.encryption_spinner);
606         EditText fileNameEditText = findViewById(R.id.file_name_edittext);
607
608         // Instantiate the import export database helper.
609         ImportExportDatabaseHelper importExportDatabaseHelper = new ImportExportDatabaseHelper();
610
611         // Get the import file.
612         File importFile = new File(fileNameEditText.getText().toString());
613
614         // Initialize the import status string
615         String importStatus = "";
616
617         // Import according to the encryption type.
618         switch (encryptionSpinner.getSelectedItemPosition()) {
619             case NO_ENCRYPTION:
620                 // Import the unencrypted file.
621                 importStatus = importExportDatabaseHelper.importUnencrypted(importFile, this);
622                 break;
623
624             case PASSWORD_ENCRYPTION:
625                 // Use a private temporary import location.
626                 File temporaryUnencryptedImportFile = new File(getApplicationContext().getCacheDir() + "/import.temp");
627
628                 try {
629                     // Create an encrypted import file input stream.
630                     FileInputStream encryptedImportFileInputStream = new FileInputStream(importFile);
631
632                     // Delete the temporary import file if it exists.
633                     if (temporaryUnencryptedImportFile.exists()) {
634                         //noinspection ResultOfMethodCallIgnored
635                         temporaryUnencryptedImportFile.delete();
636                     }
637
638                     // Create an unencrypted import file output stream.
639                     FileOutputStream unencryptedImportFileOutputStream = new FileOutputStream(temporaryUnencryptedImportFile);
640
641                     // Get a handle for the encryption password EditText.
642                     EditText encryptionPasswordEditText = findViewById(R.id.password_encryption_edittext);
643
644                     // Get the encryption password.
645                     String encryptionPasswordString = encryptionPasswordEditText.getText().toString();
646
647                     // Get the salt from the beginning of the import file.
648                     byte[] saltByteArray = new byte[32];
649                     //noinspection ResultOfMethodCallIgnored
650                     encryptedImportFileInputStream.read(saltByteArray);
651
652                     // Get the initialization vector from the import file.
653                     byte[] initializationVector = new byte[12];
654                     //noinspection ResultOfMethodCallIgnored
655                     encryptedImportFileInputStream.read(initializationVector);
656
657                     // Convert the encryption password to a byte array.
658                     byte[] encryptionPasswordByteArray = encryptionPasswordString.getBytes("UTF-8");
659
660                     // Append the salt to the encryption password byte array.  This protects against rainbow table attacks.
661                     byte[] encryptionPasswordWithSaltByteArray = new byte[encryptionPasswordByteArray.length + saltByteArray.length];
662                     System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.length);
663                     System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.length, saltByteArray.length);
664
665                     // Get a SHA-512 message digest.
666                     MessageDigest messageDigest = MessageDigest.getInstance("SHA-512");
667
668                     // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
669                     byte[] hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray);
670
671                     // Truncate the encryption password byte array to 256 bits (32 bytes).
672                     byte[] truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32);
673
674                     // Create an AES secret key from the encryption password byte array.
675                     SecretKeySpec secretKey = new SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES");
676
677                     // 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.
678                     Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
679
680                     // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
681                     GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initializationVector);
682
683                     // Initialize the cipher.
684                     cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
685
686                     // Create a cipher input stream.
687                     CipherInputStream cipherInputStream = new CipherInputStream(encryptedImportFileInputStream, cipher);
688
689                     // 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.
690                     int numberOfBytesRead;
691                     byte[] decryptedBytes = new byte[16];
692
693                     // 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.
694                     while ((numberOfBytesRead = cipherInputStream.read(decryptedBytes)) != -1) {
695                         // Write the data to the unencrypted import file output stream.
696                         unencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead);
697                     }
698
699                     // Close the streams.
700                     unencryptedImportFileOutputStream.flush();
701                     unencryptedImportFileOutputStream.close();
702                     cipherInputStream.close();
703                     encryptedImportFileInputStream.close();
704
705                     // Wipe the encryption data from memory.
706                     //noinspection UnusedAssignment
707                     encryptionPasswordString = "";
708                     Arrays.fill(saltByteArray, (byte) 0);
709                     Arrays.fill(initializationVector, (byte) 0);
710                     Arrays.fill(encryptionPasswordByteArray, (byte) 0);
711                     Arrays.fill(encryptionPasswordWithSaltByteArray, (byte) 0);
712                     Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, (byte) 0);
713                     Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, (byte) 0);
714                     Arrays.fill(decryptedBytes, (byte) 0);
715
716                     // Import the unencrypted database from the private location.
717                     importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFile, this);
718
719                     // Delete the temporary unencrypted import file.
720                     //noinspection ResultOfMethodCallIgnored
721                     temporaryUnencryptedImportFile.delete();
722                 } catch (Exception exception) {
723                     importStatus = exception.toString();
724                 }
725                 break;
726
727             case GPG_ENCRYPTION:
728
729                 break;
730         }
731
732         // Respond to the import disposition.
733         if (importStatus.equals(ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)) {  // The import was successful.
734             // Create an intent to restart Privacy Browser.
735             Intent restartIntent = getParentActivityIntent();
736
737             // Assert that the intent is not null to remove the lint error below.
738             assert restartIntent != null;
739
740             // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
741             restartIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
742
743             // Make it so.
744             startActivity(restartIntent);
745         } else {  // The import was not successful.
746             // Display a snack bar with the import error.
747             Snackbar.make(fileNameEditText, getString(R.string.import_failed) + "  " + importStatus, Snackbar.LENGTH_INDEFINITE).show();
748         }
749     }
750 }