2 * Copyright 2018-2023 Soren Stoutner <soren@stoutner.com>.
4 * This file is part of Privacy Browser Android <https://www.stoutner.com/privacy-browser-android>.
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.
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.
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/>.
20 package com.stoutner.privacybrowser.activities
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
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
47 import com.google.android.material.snackbar.Snackbar
48 import com.google.android.material.textfield.TextInputLayout
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
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
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
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
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"
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
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
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()
115 // Set the file name name text.
116 fileNameEditText.setText(fileNameString)
118 // Move the cursor to the end of the file name edit text.
119 fileNameEditText.setSelection(fileNameString.length)
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()
129 // Set the file name name text.
130 fileNameEditText.setText(fileNameString)
132 // Move the cursor to the end of the file name edit text.
133 fileNameEditText.setSelection(fileNameString.length)
137 private val openKeychainDecryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
138 // Delete the temporary PGP encrypted import file.
139 if (temporaryPgpEncryptedImportFile.exists())
140 temporaryPgpEncryptedImportFile.delete()
142 // Delete the file provider directory if it exists.
143 if (fileProviderDirectory.exists())
144 fileProviderDirectory.delete()
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()
152 // Delete the file provider directory if it exists.
153 if (fileProviderDirectory.exists())
154 fileProviderDirectory.delete()
157 public override fun onCreate(savedInstanceState: Bundle?) {
158 // Get a handle for the shared preferences.
159 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
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)
165 // Disable screenshots if not allowed.
166 if (!allowScreenshots)
167 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
169 // Run the default commands.
170 super.onCreate(savedInstanceState)
172 // Set the content view.
174 setContentView(R.layout.import_export_bottom_appbar)
176 setContentView(R.layout.import_export_top_appbar)
178 // Get a handle for the toolbar.
179 val toolbar = findViewById<Toolbar>(R.id.import_export_toolbar)
181 // Set the support action bar.
182 setSupportActionBar(toolbar)
184 // Get a handle for the action bar.
185 val actionBar = supportActionBar!!
187 // Display the home arrow on the support action bar.
188 actionBar.setDisplayHomeAsUpEnabled(true)
190 // Find out if OpenKeychain is installed.
191 openKeychainInstalled = try {
192 // The newer method can be used once the minimum API >= 33.
193 @Suppress("DEPRECATION")
194 packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
195 } catch (exception: PackageManager.NameNotFoundException) {
199 // Get handles for the views.
200 encryptionSpinner = findViewById(R.id.encryption_spinner)
201 encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
202 encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
203 openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
204 fileLocationCardView = findViewById(R.id.file_location_cardview)
205 importRadioButton = findViewById(R.id.import_radiobutton)
206 val exportRadioButton = findViewById<RadioButton>(R.id.export_radiobutton)
207 fileNameLinearLayout = findViewById(R.id.file_name_linearlayout)
208 fileNameEditText = findViewById(R.id.file_name_edittext)
209 openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
210 importExportButton = findViewById(R.id.import_export_button)
212 // Create an array adapter for the spinner.
213 val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
215 // Set the drop down view resource on the spinner.
216 encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
218 // Set the array adapter for the spinner.
219 encryptionSpinner.adapter = encryptionArrayAdapter
221 // Update the UI when the spinner changes.
222 encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
223 override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
226 // Hide the unneeded layout items.
227 encryptionPasswordTextInputLayout.visibility = View.GONE
228 openKeychainRequiredTextView.visibility = View.GONE
229 openKeychainImportInstructionsTextView.visibility = View.GONE
231 // Show the file location card.
232 fileLocationCardView.visibility = View.VISIBLE
234 // Show the file name linear layout if either import or export is checked.
235 if (importRadioButton.isChecked || exportRadioButton.isChecked)
236 fileNameLinearLayout.visibility = View.VISIBLE
238 // Reset the text of the import button, which may have been changed to `Decrypt`.
239 if (importRadioButton.isChecked)
240 importExportButton.setText(R.string.import_button)
242 // Clear the file name edit text.
243 fileNameEditText.text.clear()
245 // Disable the import/export button.
246 importExportButton.isEnabled = false
249 PASSWORD_ENCRYPTION -> {
250 // Hide the OpenPGP layout items.
251 openKeychainRequiredTextView.visibility = View.GONE
252 openKeychainImportInstructionsTextView.visibility = View.GONE
254 // Show the password encryption layout items.
255 encryptionPasswordTextInputLayout.visibility = View.VISIBLE
257 // Show the file location card.
258 fileLocationCardView.visibility = View.VISIBLE
260 // Show the file name linear layout if either import or export is checked.
261 if (importRadioButton.isChecked || exportRadioButton.isChecked)
262 fileNameLinearLayout.visibility = View.VISIBLE
264 // Reset the text of the import button, which may have been changed to `Decrypt`.
265 if (importRadioButton.isChecked)
266 importExportButton.setText(R.string.import_button)
268 // Clear the file name edit text.
269 fileNameEditText.text.clear()
271 // Disable the import/export button.
272 importExportButton.isEnabled = false
275 OPENPGP_ENCRYPTION -> {
276 // Hide the password encryption layout items.
277 encryptionPasswordTextInputLayout.visibility = View.GONE
279 // Updated items based on the installation status of OpenKeychain.
280 if (openKeychainInstalled) { // OpenKeychain is installed.
281 // Show the file location card.
282 fileLocationCardView.visibility = View.VISIBLE
284 // Update the layout based on the checked radio button.
285 if (importRadioButton.isChecked) {
286 // Show the file name linear layout and the OpenKeychain import instructions.
287 fileNameLinearLayout.visibility = View.VISIBLE
288 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
290 // Set the text of the import button to be `Decrypt`.
291 importExportButton.setText(R.string.decrypt)
293 // Clear the file name edit text.
294 fileNameEditText.text.clear()
296 // Disable the import/export button.
297 importExportButton.isEnabled = false
298 } else if (exportRadioButton.isChecked) {
299 // Hide the file name linear layout and the OpenKeychain import instructions.
300 fileNameLinearLayout.visibility = View.GONE
301 openKeychainImportInstructionsTextView.visibility = View.GONE
303 // Enable the export button.
304 importExportButton.isEnabled = true
306 } else { // OpenKeychain is not installed.
307 // Show the OpenPGP required layout item.
308 openKeychainRequiredTextView.visibility = View.VISIBLE
310 // Hide the file location card.
311 fileLocationCardView.visibility = View.GONE
317 override fun onNothingSelected(parent: AdapterView<*>?) {}
320 // Update the status of the import/export button when the password changes.
321 encryptionPasswordEditText.addTextChangedListener(object : TextWatcher {
322 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
326 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
330 override fun afterTextChanged(s: Editable) {
331 // Enable the import/export button if both the file string and the password are populated.
332 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
336 // Update the UI when the file name edit text changes.
337 fileNameEditText.addTextChangedListener(object : TextWatcher {
338 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
342 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
346 override fun afterTextChanged(s: Editable) {
347 // Adjust the UI according to the encryption spinner position.
348 if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
349 // Enable the import/export button if both the file name and the password are populated.
350 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
352 // Enable the export button if the file name is populated.
353 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
358 // Check to see if the activity has been restarted.
359 if (savedInstanceState == null) { // The app has not been restarted.
360 // Initially hide the unneeded views.
361 encryptionPasswordTextInputLayout.visibility = View.GONE
362 openKeychainRequiredTextView.visibility = View.GONE
363 fileNameLinearLayout.visibility = View.GONE
364 openKeychainImportInstructionsTextView.visibility = View.GONE
365 importExportButton.visibility = View.GONE
366 } else { // The app has been restarted.
367 // Restore the visibility of the views.
368 encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
369 openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
370 fileLocationCardView.visibility = savedInstanceState.getInt(FILE_LOCATION_CARD_VIEW)
371 fileNameLinearLayout.visibility = savedInstanceState.getInt(FILE_NAME_LINEARLAYOUT_VISIBILITY)
372 openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
373 importExportButton.visibility = savedInstanceState.getInt(IMPORT_EXPORT_BUTTON_VISIBILITY)
376 fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
377 importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
381 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
382 // Run the default commands.
383 super.onSaveInstanceState(savedInstanceState)
385 // Save the visibility of the views.
386 savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
387 savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
388 savedInstanceState.putInt(FILE_LOCATION_CARD_VIEW, fileLocationCardView.visibility)
389 savedInstanceState.putInt(FILE_NAME_LINEARLAYOUT_VISIBILITY, fileNameLinearLayout.visibility)
390 savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
391 savedInstanceState.putInt(IMPORT_EXPORT_BUTTON_VISIBILITY, importExportButton.visibility)
394 savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
395 savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
398 fun onClickRadioButton(view: View) {
399 // Check to see if import or export was selected.
400 if (view.id == R.id.import_radiobutton) { // The import radio button is selected.
401 // Check to see if OpenPGP encryption is selected.
402 if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption selected.
403 // Show the OpenKeychain import instructions.
404 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
406 // Set the text on the import/export button to be `Decrypt`.
407 importExportButton.setText(R.string.decrypt)
408 } else { // OpenPGP encryption not selected.
409 // Hide the OpenKeychain import instructions.
410 openKeychainImportInstructionsTextView.visibility = View.GONE
412 // Set the text on the import/export button to be `Import`.
413 importExportButton.setText(R.string.import_button)
416 // Display the file name views.
417 fileNameLinearLayout.visibility = View.VISIBLE
418 importExportButton.visibility = View.VISIBLE
420 // Clear the file name edit text.
421 fileNameEditText.text.clear()
423 // Disable the import/export button.
424 importExportButton.isEnabled = false
425 } else { // The export radio button is selected.
426 // Hide the OpenKeychain import instructions.
427 openKeychainImportInstructionsTextView.visibility = View.GONE
429 // Set the text on the import/export button to be `Export`.
430 importExportButton.setText(R.string.export)
432 // Show the import/export button.
433 importExportButton.visibility = View.VISIBLE
435 // Check to see if OpenPGP encryption is selected.
436 if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption is selected.
437 // Hide the file name views.
438 fileNameLinearLayout.visibility = View.GONE
440 // Enable the export button.
441 importExportButton.isEnabled = true
442 } else { // OpenPGP encryption is not selected.
443 // Show the file name view.
444 fileNameLinearLayout.visibility = View.VISIBLE
446 // Clear the file name edit text.
447 fileNameEditText.text.clear()
449 // Disable the import/export button.
450 importExportButton.isEnabled = false
455 fun browse(@Suppress("UNUSED_PARAMETER") view: View) {
456 // Check to see if import or export is selected.
457 if (importRadioButton.isChecked) { // Import is selected.
458 // Open the file picker.
459 browseForImportActivityResultLauncher.launch("*/*")
460 } else { // Export is selected
461 // Open the file picker with the export name according to the encryption type.
462 if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION) // No encryption is selected.
463 browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
464 else // Password encryption is selected.
465 browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
469 fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
470 // Instantiate the import export database helper.
471 val importExportDatabaseHelper = ImportExportDatabaseHelper()
473 // Check to see if import or export is selected.
474 if (importRadioButton.isChecked) { // Import is selected.
475 // Initialize the import status string
476 var importStatus = ""
478 // Get the file name string.
479 val fileNameString = fileNameEditText.text.toString()
481 // Import according to the encryption type.
482 when (encryptionSpinner.selectedItemPosition) {
485 // Get an input stream for the file name.
486 // A file may be opened directly once the minimum API >= 29. <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
487 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
489 // Import the unencrypted file.
490 importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
492 // Close the input stream.
494 } catch (exception: FileNotFoundException) {
495 // Update the import status.
496 importStatus = exception.toString()
499 // Restart Privacy Browser if successful.
500 if (importStatus == IMPORT_SUCCESSFUL)
501 restartPrivacyBrowser()
504 PASSWORD_ENCRYPTION -> {
506 // Get the encryption password.
507 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
509 // Get an input stream for the file name.
510 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
512 // Initialize a salt byte array. Salt protects against rainbow table attacks.
513 val saltByteArray = ByteArray(32)
515 // Get the salt from the beginning of the import file.
516 inputStream.read(saltByteArray)
518 // Create an initialization vector.
519 val initializationVector = ByteArray(12)
521 // Get the initialization vector from the import file.
522 inputStream.read(initializationVector)
524 // Convert the encryption password to a byte array.
525 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
527 // Create an encryption password with salt byte array.
528 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
530 // Populate the first part of the encryption password with salt byte array with the encryption password.
531 System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
533 // Populate the second part of the encryption password with salt byte array with the salt.
534 System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
536 // Get a SHA-512 message digest.
537 val messageDigest = MessageDigest.getInstance("SHA-512")
539 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
540 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
542 // Truncate the encryption password byte array to 256 bits (32 bytes).
543 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
545 // Create an AES secret key from the encryption password byte array.
546 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
548 // Get a Advanced Encryption Standard, Galois/Counter Mode, No Padding cipher instance. Galois/Counter mode protects against modification of the ciphertext. It doesn't use padding.
549 val cipher = Cipher.getInstance("AES/GCM/NoPadding")
551 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
552 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
554 // Initialize the cipher.
555 cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
557 // Create a cipher input stream.
558 val cipherInputStream = CipherInputStream(inputStream, cipher)
560 // Initialize variables to store data as it is moved from the cipher input stream to the unencrypted import file output stream. Move 128 bits (16 bytes) at a time.
561 var numberOfBytesRead: Int
562 val decryptedBytes = ByteArray(16)
564 // Create a private temporary unencrypted import file.
565 val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
567 // Create an temporary unencrypted import file output stream.
568 val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
570 // Read up to 128 bits (16 bytes) of data from the cipher input stream. `-1` will be returned when the end of the file is reached.
571 while (cipherInputStream.read(decryptedBytes).also { numberOfBytesRead = it } != -1) {
572 // Write the data to the temporary unencrypted import file output stream.
573 temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead)
576 // Flush the temporary unencrypted import file output stream.
577 temporaryUnencryptedImportFileOutputStream.flush()
579 // Close the streams.
580 temporaryUnencryptedImportFileOutputStream.close()
581 cipherInputStream.close()
584 // Wipe the encryption data from memory.
585 Arrays.fill(saltByteArray, 0.toByte())
586 Arrays.fill(initializationVector, 0.toByte())
587 Arrays.fill(encryptionPasswordByteArray, 0.toByte())
588 Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
589 Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
590 Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
591 Arrays.fill(decryptedBytes, 0.toByte())
593 // Create a temporary unencrypted import file input stream.
594 val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
596 // Import the temporary unencrypted import file.
597 importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
599 // Close the temporary unencrypted import file input stream.
600 temporaryUnencryptedImportFileInputStream.close()
602 // Delete the temporary unencrypted import file.
603 temporaryUnencryptedImportFile.delete()
605 // Restart Privacy Browser if successful.
606 if (importStatus == IMPORT_SUCCESSFUL)
607 restartPrivacyBrowser()
608 } catch (exception: Exception) {
609 // Update the import status.
610 importStatus = exception.toString()
614 OPENPGP_ENCRYPTION -> {
616 // Get a handle for the file provider directory.
617 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
619 // Create the file provider directory. Any errors will be handled by the catch statement below.
620 fileProviderDirectory.mkdir()
622 // Set the temporary PGP encrypted import file.
623 temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
625 // Create a temporary PGP encrypted import file output stream.
626 val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
628 // Get an input stream for the file name.
629 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
631 // Create a transfer byte array.
632 val transferByteArray = ByteArray(1024)
634 // Create an integer to track the number of bytes read.
637 // Copy the input stream to the temporary PGP encrypted import file.
638 while (inputStream.read(transferByteArray).also { bytesRead = it } > 0)
639 temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead)
641 // Flush the temporary PGP encrypted import file output stream.
642 temporaryPgpEncryptedImportFileOutputStream.flush()
644 // Close the streams.
646 temporaryPgpEncryptedImportFileOutputStream.close()
648 // Create a decryption intent for OpenKeychain.
649 val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
651 // Include the URI to be decrypted.
652 openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
654 // Allow OpenKeychain to read the file URI.
655 openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
657 // Send the intent to the OpenKeychain package.
658 openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
661 openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
663 // Update the import status.
664 importStatus = IMPORT_SUCCESSFUL
665 } catch (exception: Exception) {
666 // Update the import status.
667 importStatus = exception.toString()
672 // Display a snack bar with the import error if it was unsuccessful.
673 if (importStatus != IMPORT_SUCCESSFUL)
674 Snackbar.make(fileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
675 } else { // Export is selected.
676 // Export according to the encryption type.
677 when (encryptionSpinner.selectedItemPosition) {
679 // Get the file name string.
680 val noEncryptionFileNameString = fileNameEditText.text.toString()
683 // Get the export file output stream.
684 // A file may be opened directly once the minimum API >= 29. <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
685 val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString))!!
687 // Export the unencrypted file.
688 val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
690 // Close the output stream.
691 exportFileOutputStream.close()
693 // Display an export disposition snackbar.
694 if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
695 Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
697 Snackbar.make(fileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
698 } catch (fileNotFoundException: FileNotFoundException) {
699 // Display a snackbar with the exception.
700 Snackbar.make(fileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
704 PASSWORD_ENCRYPTION -> {
706 // Create a temporary unencrypted export file.
707 val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
709 // Create a temporary unencrypted export output stream.
710 val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
712 // Populate the temporary unencrypted export.
713 val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
715 // Close the temporary unencrypted export output stream.
716 temporaryUnencryptedExportOutputStream.close()
718 // Create an unencrypted export file input stream.
719 val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
721 // Get the encryption password.
722 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
724 // Initialize a secure random number generator.
725 val secureRandom = SecureRandom()
727 // Initialize a salt byte array. Salt protects against rainbow table attacks.
728 val saltByteArray = ByteArray(32)
730 // Get a 256 bit (32 byte) random salt.
731 secureRandom.nextBytes(saltByteArray)
733 // Convert the encryption password to a byte array.
734 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
736 // Create an encryption password with salt byte array.
737 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
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)
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)
745 // Get a SHA-512 message digest.
746 val messageDigest = MessageDigest.getInstance("SHA-512")
748 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
749 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
751 // Truncate the encryption password byte array to 256 bits (32 bytes).
752 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
754 // Create an AES secret key from the encryption password byte array.
755 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
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)
760 // Populate the initialization vector with random data.
761 secureRandom.nextBytes(initializationVector)
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")
766 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
767 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
769 // Initialize the cipher.
770 cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
772 // Get the file name string.
773 val passwordEncryptionFileNameString = fileNameEditText.text.toString()
775 // Get the export file output stream.
776 val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!!
778 // Add the salt and the initialization vector to the export file output stream.
779 exportFileOutputStream.write(saltByteArray)
780 exportFileOutputStream.write(initializationVector)
782 // Create a cipher output stream.
783 val cipherOutputStream = CipherOutputStream(exportFileOutputStream, cipher)
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)
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)
794 // Close the streams.
795 cipherOutputStream.flush()
796 cipherOutputStream.close()
797 exportFileOutputStream.close()
798 unencryptedExportFileInputStream.close()
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())
809 // Delete the temporary unencrypted export file.
810 temporaryUnencryptedExportFile.delete()
812 // Display an export disposition snackbar.
813 if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
814 Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
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()
823 OPENPGP_ENCRYPTION -> {
825 // Get a handle for the file provider directory.
826 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
828 // Create the file provider directory. Any errors will be handled by the catch statement below.
829 fileProviderDirectory.mkdir()
831 // Set the temporary pre-encrypted export file.
832 temporaryPreEncryptedExportFile = File(fileProviderDirectory.toString() + "/" +
833 getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
835 // Delete the temporary pre-encrypted export file if it already exists.
836 if (temporaryPreEncryptedExportFile.exists())
837 temporaryPreEncryptedExportFile.delete()
839 // Create a temporary pre-encrypted export output stream.
840 val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
842 // Populate the temporary pre-encrypted export file.
843 val openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this)
845 // Flush the temporary pre-encryption export output stream.
846 temporaryPreEncryptedExportOutputStream.flush()
848 // Close the temporary pre-encryption export output stream.
849 temporaryPreEncryptedExportOutputStream.close()
851 // Display an export error snackbar if the temporary pre-encrypted export failed.
852 if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
853 Snackbar.make(fileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
855 // Create an encryption intent for OpenKeychain.
856 val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
858 // Include the temporary unencrypted export file URI.
859 openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
861 // Allow OpenKeychain to read the file URI.
862 openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
864 // Send the intent to the OpenKeychain package.
865 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
868 openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
869 } catch (exception: Exception) {
870 // Display a snackbar with the exception.
871 Snackbar.make(fileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
878 private fun restartPrivacyBrowser() {
879 // Create an intent to restart Privacy Browser.
880 val restartIntent = parentActivityIntent!!
882 // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack. It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
883 restartIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
885 // Create a restart handler.
886 val restartHandler = Handler(mainLooper)
888 // Create a restart runnable.
889 val restartRunnable = Runnable {
891 // Restart Privacy Browser.
892 startActivity(restartIntent)
894 // Kill this instance of Privacy Browser. Otherwise, the app exhibits sporadic behavior after the restart.
898 // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
899 restartHandler.postDelayed(restartRunnable, 150)