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