]> gitweb.stoutner.com Git - PrivacyBrowserAndroid.git/blob - app/src/main/java/com/stoutner/privacybrowser/activities/ImportExportActivity.kt
Bump the target API to 34 (Android 14). https://redmine.stoutner.com/issues/1107
[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             packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
193         } catch (exception: PackageManager.NameNotFoundException) {
194             false
195         }
196
197         // Get handles for the views.
198         encryptionSpinner = findViewById(R.id.encryption_spinner)
199         encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
200         encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
201         openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
202         fileLocationCardView = findViewById(R.id.file_location_cardview)
203         importRadioButton = findViewById(R.id.import_radiobutton)
204         val exportRadioButton = findViewById<RadioButton>(R.id.export_radiobutton)
205         fileNameLinearLayout = findViewById(R.id.file_name_linearlayout)
206         fileNameEditText = findViewById(R.id.file_name_edittext)
207         openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
208         importExportButton = findViewById(R.id.import_export_button)
209
210         // Create an array adapter for the spinner.
211         val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
212
213         // Set the drop down view resource on the spinner.
214         encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
215
216         // Set the array adapter for the spinner.
217         encryptionSpinner.adapter = encryptionArrayAdapter
218
219         // Update the UI when the spinner changes.
220         encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
221             override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
222                 when (position) {
223                     NO_ENCRYPTION -> {
224                         // Hide the unneeded layout items.
225                         encryptionPasswordTextInputLayout.visibility = View.GONE
226                         openKeychainRequiredTextView.visibility = View.GONE
227                         openKeychainImportInstructionsTextView.visibility = View.GONE
228
229                         // Show the file location card.
230                         fileLocationCardView.visibility = View.VISIBLE
231
232                         // Show the file name linear layout if either import or export is checked.
233                         if (importRadioButton.isChecked || exportRadioButton.isChecked)
234                             fileNameLinearLayout.visibility = View.VISIBLE
235
236                         // Reset the text of the import button, which may have been changed to `Decrypt`.
237                         if (importRadioButton.isChecked)
238                             importExportButton.setText(R.string.import_button)
239
240                         // Clear the file name edit text.
241                         fileNameEditText.text.clear()
242
243                         // Disable the import/export button.
244                         importExportButton.isEnabled = false
245                     }
246
247                     PASSWORD_ENCRYPTION -> {
248                         // Hide the OpenPGP layout items.
249                         openKeychainRequiredTextView.visibility = View.GONE
250                         openKeychainImportInstructionsTextView.visibility = View.GONE
251
252                         // Show the password encryption layout items.
253                         encryptionPasswordTextInputLayout.visibility = View.VISIBLE
254
255                         // Show the file location card.
256                         fileLocationCardView.visibility = View.VISIBLE
257
258                         // Show the file name linear layout if either import or export is checked.
259                         if (importRadioButton.isChecked || exportRadioButton.isChecked)
260                             fileNameLinearLayout.visibility = View.VISIBLE
261
262                         // Reset the text of the import button, which may have been changed to `Decrypt`.
263                         if (importRadioButton.isChecked)
264                             importExportButton.setText(R.string.import_button)
265
266                         // Clear the file name edit text.
267                         fileNameEditText.text.clear()
268
269                         // Disable the import/export button.
270                         importExportButton.isEnabled = false
271                     }
272
273                     OPENPGP_ENCRYPTION -> {
274                         // Hide the password encryption layout items.
275                         encryptionPasswordTextInputLayout.visibility = View.GONE
276
277                         // Updated items based on the installation status of OpenKeychain.
278                         if (openKeychainInstalled) {  // OpenKeychain is installed.
279                             // Show the file location card.
280                             fileLocationCardView.visibility = View.VISIBLE
281
282                             // Update the layout based on the checked radio button.
283                             if (importRadioButton.isChecked) {
284                                 // Show the file name linear layout and the OpenKeychain import instructions.
285                                 fileNameLinearLayout.visibility = View.VISIBLE
286                                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
287
288                                 // Set the text of the import button to be `Decrypt`.
289                                 importExportButton.setText(R.string.decrypt)
290
291                                 // Clear the file name edit text.
292                                 fileNameEditText.text.clear()
293
294                                 // Disable the import/export button.
295                                 importExportButton.isEnabled = false
296                             } else if (exportRadioButton.isChecked) {
297                                 // Hide the file name linear layout and the OpenKeychain import instructions.
298                                 fileNameLinearLayout.visibility = View.GONE
299                                 openKeychainImportInstructionsTextView.visibility = View.GONE
300
301                                 // Enable the export button.
302                                 importExportButton.isEnabled = true
303                             }
304                         } else {  // OpenKeychain is not installed.
305                             // Show the OpenPGP required layout item.
306                             openKeychainRequiredTextView.visibility = View.VISIBLE
307
308                             // Hide the file location card.
309                             fileLocationCardView.visibility = View.GONE
310                         }
311                     }
312                 }
313             }
314
315             override fun onNothingSelected(parent: AdapterView<*>?) {}
316         }
317
318         // Update the status of the import/export button when the password changes.
319         encryptionPasswordEditText.addTextChangedListener(object : TextWatcher {
320             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
321                 // Do nothing.
322             }
323
324             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
325                 // Do nothing.
326             }
327
328             override fun afterTextChanged(s: Editable) {
329                 // Enable the import/export button if both the file string and the password are populated.
330                 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
331             }
332         })
333
334         // Update the UI when the file name edit text changes.
335         fileNameEditText.addTextChangedListener(object : TextWatcher {
336             override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
337                 // Do nothing.
338             }
339
340             override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
341                 // Do nothing.
342             }
343
344             override fun afterTextChanged(s: Editable) {
345                 // Adjust the UI according to the encryption spinner position.
346                 if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
347                     // Enable the import/export button if both the file name and the password are populated.
348                     importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
349                 } else {
350                     // Enable the export button if the file name is populated.
351                     importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
352                 }
353             }
354         })
355
356         // Check to see if the activity has been restarted.
357         if (savedInstanceState == null) {  // The app has not been restarted.
358             // Initially hide the unneeded views.
359             encryptionPasswordTextInputLayout.visibility = View.GONE
360             openKeychainRequiredTextView.visibility = View.GONE
361             fileNameLinearLayout.visibility = View.GONE
362             openKeychainImportInstructionsTextView.visibility = View.GONE
363             importExportButton.visibility = View.GONE
364         } else {  // The app has been restarted.
365             // Restore the visibility of the views.
366             encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
367             openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
368             fileLocationCardView.visibility = savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW)
369             fileNameLinearLayout.visibility = savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY)
370             openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
371             importExportButton.visibility = savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY)
372
373             // Restore the text.
374             fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
375             importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
376         }
377     }
378
379     public override fun onSaveInstanceState(savedInstanceState: Bundle) {
380         // Run the default commands.
381         super.onSaveInstanceState(savedInstanceState)
382
383         // Save the visibility of the views.
384         savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
385         savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
386         savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.visibility)
387         savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.visibility)
388         savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
389         savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.visibility)
390
391         // Save the text.
392         savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
393         savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
394     }
395
396     fun onClickRadioButton(view: View) {
397         // Check to see if import or export was selected.
398         if (view.id == R.id.import_radiobutton) {  // The import radio button is selected.
399             // Check to see if OpenPGP encryption is selected.
400             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption selected.
401                 // Show the OpenKeychain import instructions.
402                 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
403
404                 // Set the text on the import/export button to be `Decrypt`.
405                 importExportButton.setText(R.string.decrypt)
406             } else {  // OpenPGP encryption not selected.
407                 // Hide the OpenKeychain import instructions.
408                 openKeychainImportInstructionsTextView.visibility = View.GONE
409
410                 // Set the text on the import/export button to be `Import`.
411                 importExportButton.setText(R.string.import_button)
412             }
413
414             // Display the file name views.
415             fileNameLinearLayout.visibility = View.VISIBLE
416             importExportButton.visibility = View.VISIBLE
417
418             // Clear the file name edit text.
419             fileNameEditText.text.clear()
420
421             // Disable the import/export button.
422             importExportButton.isEnabled = false
423         } else {  // The export radio button is selected.
424             // Hide the OpenKeychain import instructions.
425             openKeychainImportInstructionsTextView.visibility = View.GONE
426
427             // Set the text on the import/export button to be `Export`.
428             importExportButton.setText(R.string.export)
429
430             // Show the import/export button.
431             importExportButton.visibility = View.VISIBLE
432
433             // Check to see if OpenPGP encryption is selected.
434             if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) {  // OpenPGP encryption is selected.
435                 // Hide the file name views.
436                 fileNameLinearLayout.visibility = View.GONE
437
438                 // Enable the export button.
439                 importExportButton.isEnabled = true
440             } else {  // OpenPGP encryption is not selected.
441                 // Show the file name view.
442                 fileNameLinearLayout.visibility = View.VISIBLE
443
444                 // Clear the file name edit text.
445                 fileNameEditText.text.clear()
446
447                 // Disable the import/export button.
448                 importExportButton.isEnabled = false
449             }
450         }
451     }
452
453     fun browse(@Suppress("UNUSED_PARAMETER") view: View) {
454         // Check to see if import or export is selected.
455         if (importRadioButton.isChecked) {  // Import is selected.
456             // Open the file picker.
457             browseForImportActivityResultLauncher.launch("*/*")
458         } else {  // Export is selected
459             // Open the file picker with the export name according to the encryption type.
460             if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION)  // No encryption is selected.
461                 browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
462             else  // Password encryption is selected.
463                 browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
464         }
465     }
466
467     fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
468         // Instantiate the import export database helper.
469         val importExportDatabaseHelper = ImportExportDatabaseHelper()
470
471         // Check to see if import or export is selected.
472         if (importRadioButton.isChecked) {  // Import is selected.
473             // Initialize the import status string
474             var importStatus = ""
475
476             // Get the file name string.
477             val fileNameString = fileNameEditText.text.toString()
478
479             // Import according to the encryption type.
480             when (encryptionSpinner.selectedItemPosition) {
481                 NO_ENCRYPTION -> {
482                     try {
483                         // Get an input stream for the file name.
484                         // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
485                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
486
487                         // Import the unencrypted file.
488                         importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
489
490                         // Close the input stream.
491                         inputStream.close()
492                     } catch (exception: FileNotFoundException) {
493                         // Update the import status.
494                         importStatus = exception.toString()
495                     }
496
497                     // Restart Privacy Browser if successful.
498                     if (importStatus == IMPORT_SUCCESSFUL)
499                         restartPrivacyBrowser()
500                 }
501
502                 PASSWORD_ENCRYPTION -> {
503                     try {
504                         // Get the encryption password.
505                         val encryptionPasswordString = encryptionPasswordEditText.text.toString()
506
507                         // Get an input stream for the file name.
508                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
509
510                         // Initialize a salt byte array.  Salt protects against rainbow table attacks.
511                         val saltByteArray = ByteArray(32)
512
513                         // Get the salt from the beginning of the import file.
514                         inputStream.read(saltByteArray)
515
516                         // Create an initialization vector.
517                         val initializationVector = ByteArray(12)
518
519                         // Get the initialization vector from the import file.
520                         inputStream.read(initializationVector)
521
522                         // Convert the encryption password to a byte array.
523                         val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
524
525                         // Create an encryption password with salt byte array.
526                         val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
527
528                         // Populate the first part of the encryption password with salt byte array with the encryption password.
529                         System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
530
531                         // Populate the second part of the encryption password with salt byte array with the salt.
532                         System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
533
534                         // Get a SHA-512 message digest.
535                         val messageDigest = MessageDigest.getInstance("SHA-512")
536
537                         // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
538                         val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
539
540                         // Truncate the encryption password byte array to 256 bits (32 bytes).
541                         val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
542
543                         // Create an AES secret key from the encryption password byte array.
544                         val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
545
546                         // 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.
547                         val cipher = Cipher.getInstance("AES/GCM/NoPadding")
548
549                         // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
550                         val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
551
552                         // Initialize the cipher.
553                         cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
554
555                         // Create a cipher input stream.
556                         val cipherInputStream = CipherInputStream(inputStream, cipher)
557
558                         // 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.
559                         var numberOfBytesRead: Int
560                         val decryptedBytes = ByteArray(16)
561
562                         // Create a private temporary unencrypted import file.
563                         val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
564
565                         // Create an temporary unencrypted import file output stream.
566                         val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
567
568                         // 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.
569                         while (cipherInputStream.read(decryptedBytes).also { numberOfBytesRead = it } != -1) {
570                             // Write the data to the temporary unencrypted import file output stream.
571                             temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead)
572                         }
573
574                         // Flush the temporary unencrypted import file output stream.
575                         temporaryUnencryptedImportFileOutputStream.flush()
576
577                         // Close the streams.
578                         temporaryUnencryptedImportFileOutputStream.close()
579                         cipherInputStream.close()
580                         inputStream.close()
581
582                         // Wipe the encryption data from memory.
583                         Arrays.fill(saltByteArray, 0.toByte())
584                         Arrays.fill(initializationVector, 0.toByte())
585                         Arrays.fill(encryptionPasswordByteArray, 0.toByte())
586                         Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
587                         Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
588                         Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
589                         Arrays.fill(decryptedBytes, 0.toByte())
590
591                         // Create a temporary unencrypted import file input stream.
592                         val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
593
594                         // Import the temporary unencrypted import file.
595                         importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
596
597                         // Close the temporary unencrypted import file input stream.
598                         temporaryUnencryptedImportFileInputStream.close()
599
600                         // Delete the temporary unencrypted import file.
601                         temporaryUnencryptedImportFile.delete()
602
603                         // Restart Privacy Browser if successful.
604                         if (importStatus == IMPORT_SUCCESSFUL)
605                             restartPrivacyBrowser()
606                     } catch (exception: Exception) {
607                         // Update the import status.
608                         importStatus = exception.toString()
609                     }
610                 }
611
612                 OPENPGP_ENCRYPTION -> {
613                     try {
614                         // Get a handle for the file provider directory.
615                         fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
616
617                         // Create the file provider directory.  Any errors will be handled by the catch statement below.
618                         fileProviderDirectory.mkdir()
619
620                         // Set the temporary PGP encrypted import file.
621                         temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
622
623                         // Create a temporary PGP encrypted import file output stream.
624                         val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
625
626                         // Get an input stream for the file name.
627                         val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
628
629                         // Create a transfer byte array.
630                         val transferByteArray = ByteArray(1024)
631
632                         // Create an integer to track the number of bytes read.
633                         var bytesRead: Int
634
635                         // Copy the input stream to the temporary PGP encrypted import file.
636                         while (inputStream.read(transferByteArray).also { bytesRead = it } > 0)
637                             temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead)
638
639                         // Flush the temporary PGP encrypted import file output stream.
640                         temporaryPgpEncryptedImportFileOutputStream.flush()
641
642                         // Close the streams.
643                         inputStream.close()
644                         temporaryPgpEncryptedImportFileOutputStream.close()
645
646                         // Create a decryption intent for OpenKeychain.
647                         val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
648
649                         // Include the URI to be decrypted.
650                         openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
651
652                         // Allow OpenKeychain to read the file URI.
653                         openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
654
655                         // Send the intent to the OpenKeychain package.
656                         openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
657
658                         // Make it so.
659                         openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
660
661                         // Update the import status.
662                         importStatus = IMPORT_SUCCESSFUL
663                     } catch (exception: Exception) {
664                         // Update the import status.
665                         importStatus = exception.toString()
666                     }
667                 }
668             }
669
670             // Display a snack bar with the import error if it was unsuccessful.
671             if (importStatus != IMPORT_SUCCESSFUL)
672                 Snackbar.make(fileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
673         } else {  // Export is selected.
674             // Export according to the encryption type.
675             when (encryptionSpinner.selectedItemPosition) {
676                 NO_ENCRYPTION -> {
677                     // Get the file name string.
678                     val noEncryptionFileNameString = fileNameEditText.text.toString()
679
680                     try {
681                         // Get the export file output stream.
682                         // A file may be opened directly once the minimum API >= 29.  <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
683                         val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString))!!
684
685                         // Export the unencrypted file.
686                         val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
687
688                         // Close the output stream.
689                         exportFileOutputStream.close()
690
691                         // Display an export disposition snackbar.
692                         if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
693                             Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
694                         else
695                             Snackbar.make(fileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
696                     } catch (fileNotFoundException: FileNotFoundException) {
697                         // Display a snackbar with the exception.
698                         Snackbar.make(fileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
699                     }
700                 }
701
702                 PASSWORD_ENCRYPTION -> {
703                     try {
704                         // Create a temporary unencrypted export file.
705                         val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
706
707                         // Create a temporary unencrypted export output stream.
708                         val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
709
710                         // Populate the temporary unencrypted export.
711                         val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
712
713                         // Close the temporary unencrypted export output stream.
714                         temporaryUnencryptedExportOutputStream.close()
715
716                         // Create an unencrypted export file input stream.
717                         val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
718
719                         // Get the encryption password.
720                         val encryptionPasswordString = encryptionPasswordEditText.text.toString()
721
722                         // Initialize a secure random number generator.
723                         val secureRandom = SecureRandom()
724
725                         // Initialize a salt byte array.  Salt protects against rainbow table attacks.
726                         val saltByteArray = ByteArray(32)
727
728                         // Get a 256 bit (32 byte) random salt.
729                         secureRandom.nextBytes(saltByteArray)
730
731                         // Convert the encryption password to a byte array.
732                         val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
733
734                         // Create an encryption password with salt byte array.
735                         val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
736
737                         // Populate the first part of the encryption password with salt byte array with the encryption password.
738                         System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
739
740                         // Populate the second part of the encryption password with salt byte array with the salt.
741                         System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
742
743                         // Get a SHA-512 message digest.
744                         val messageDigest = MessageDigest.getInstance("SHA-512")
745
746                         // Hash the salted encryption password.  Otherwise, any characters after the 32nd character in the password are ignored.
747                         val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
748
749                         // Truncate the encryption password byte array to 256 bits (32 bytes).
750                         val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
751
752                         // Create an AES secret key from the encryption password byte array.
753                         val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
754
755                         // Create an initialization vector.  According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
756                         val initializationVector = ByteArray(12)
757
758                         // Populate the initialization vector with random data.
759                         secureRandom.nextBytes(initializationVector)
760
761                         // 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.
762                         val cipher = Cipher.getInstance("AES/GCM/NoPadding")
763
764                         // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
765                         val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
766
767                         // Initialize the cipher.
768                         cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
769
770                         // Get the file name string.
771                         val passwordEncryptionFileNameString = fileNameEditText.text.toString()
772
773                         // Get the export file output stream.
774                         val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!!
775
776                         // Add the salt and the initialization vector to the export file output stream.
777                         exportFileOutputStream.write(saltByteArray)
778                         exportFileOutputStream.write(initializationVector)
779
780                         // Create a cipher output stream.
781                         val cipherOutputStream = CipherOutputStream(exportFileOutputStream, cipher)
782
783                         // 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.
784                         var numberOfBytesRead: Int
785                         val encryptedBytes = ByteArray(16)
786
787                         // 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.
788                         while (unencryptedExportFileInputStream.read(encryptedBytes).also { numberOfBytesRead = it } != -1)
789                         // Write the data to the cipher output stream.
790                             cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead)
791
792                         // Close the streams.
793                         cipherOutputStream.flush()
794                         cipherOutputStream.close()
795                         exportFileOutputStream.close()
796                         unencryptedExportFileInputStream.close()
797
798                         // Wipe the encryption data from memory.
799                         Arrays.fill(saltByteArray, 0.toByte())
800                         Arrays.fill(encryptionPasswordByteArray, 0.toByte())
801                         Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
802                         Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
803                         Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
804                         Arrays.fill(initializationVector, 0.toByte())
805                         Arrays.fill(encryptedBytes, 0.toByte())
806
807                         // Delete the temporary unencrypted export file.
808                         temporaryUnencryptedExportFile.delete()
809
810                         // Display an export disposition snackbar.
811                         if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
812                             Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
813                         else
814                             Snackbar.make(fileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
815                     } catch (exception: Exception) {
816                         // Display a snackbar with the exception.
817                         Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
818                     }
819                 }
820
821                 OPENPGP_ENCRYPTION -> {
822                     try {
823                         // Get a handle for the file provider directory.
824                         fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
825
826                         // Create the file provider directory.  Any errors will be handled by the catch statement below.
827                         fileProviderDirectory.mkdir()
828
829                         // Set the temporary pre-encrypted export file.
830                         temporaryPreEncryptedExportFile = File(fileProviderDirectory.toString() + "/" +
831                                 getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
832
833                         // Delete the temporary pre-encrypted export file if it already exists.
834                         if (temporaryPreEncryptedExportFile.exists())
835                             temporaryPreEncryptedExportFile.delete()
836
837                         // Create a temporary pre-encrypted export output stream.
838                         val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
839
840                         // Populate the temporary pre-encrypted export file.
841                         val openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this)
842
843                         // Flush the temporary pre-encryption export output stream.
844                         temporaryPreEncryptedExportOutputStream.flush()
845
846                         // Close the temporary pre-encryption export output stream.
847                         temporaryPreEncryptedExportOutputStream.close()
848
849                         // Display an export error snackbar if the temporary pre-encrypted export failed.
850                         if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
851                             Snackbar.make(fileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
852
853                         // Create an encryption intent for OpenKeychain.
854                         val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
855
856                         // Include the temporary unencrypted export file URI.
857                         openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
858
859                         // Allow OpenKeychain to read the file URI.
860                         openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
861
862                         // Send the intent to the OpenKeychain package.
863                         openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
864
865                         // Make it so.
866                         openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
867                     } catch (exception: Exception) {
868                         // Display a snackbar with the exception.
869                         Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
870                     }
871                 }
872             }
873         }
874     }
875
876     private fun restartPrivacyBrowser() {
877         // Create an intent to restart Privacy Browser.
878         val restartIntent = parentActivityIntent!!
879
880         // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack.  It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
881         restartIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
882
883         // Create a restart handler.
884         val restartHandler = Handler(mainLooper)
885
886         // Create a restart runnable.
887         val restartRunnable = Runnable {
888
889             // Restart Privacy Browser.
890             startActivity(restartIntent)
891
892             // Kill this instance of Privacy Browser.  Otherwise, the app exhibits sporadic behavior after the restart.
893             exitProcess(0)
894         }
895
896         // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
897         restartHandler.postDelayed(restartRunnable, 150)
898     }
899 }