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