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
39 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.ImportExportDatabaseHelper
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
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
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
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"
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
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
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()
112 // Set the file name name text.
113 fileNameEditText.setText(fileNameString)
115 // Move the cursor to the end of the file name edit text.
116 fileNameEditText.setSelection(fileNameString.length)
120 private val browseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { 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()
126 // Set the file name name text.
127 fileNameEditText.setText(fileNameString)
129 // Move the cursor to the end of the file name edit text.
130 fileNameEditText.setSelection(fileNameString.length)
134 private val openKeychainDecryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
135 // Delete the temporary PGP encrypted import file.
136 if (temporaryPgpEncryptedImportFile.exists())
137 temporaryPgpEncryptedImportFile.delete()
139 // Delete the file provider directory if it exists.
140 if (fileProviderDirectory.exists())
141 fileProviderDirectory.delete()
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()
149 // Delete the file provider directory if it exists.
150 if (fileProviderDirectory.exists())
151 fileProviderDirectory.delete()
154 public override fun onCreate(savedInstanceState: Bundle?) {
155 // Get a handle for the shared preferences.
156 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
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)
162 // Disable screenshots if not allowed.
163 if (!allowScreenshots)
164 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
166 // Run the default commands.
167 super.onCreate(savedInstanceState)
169 // Set the content view.
171 setContentView(R.layout.import_export_bottom_appbar)
173 setContentView(R.layout.import_export_top_appbar)
175 // Get a handle for the toolbar.
176 val toolbar = findViewById<Toolbar>(R.id.import_export_toolbar)
178 // Set the support action bar.
179 setSupportActionBar(toolbar)
181 // Get a handle for the action bar.
182 val actionBar = supportActionBar!!
184 // Display the home arrow on the support action bar.
185 actionBar.setDisplayHomeAsUpEnabled(true)
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) {
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)
209 // Create an array adapter for the spinner.
210 val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
212 // Set the drop down view resource on the spinner.
213 encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
215 // Set the array adapter for the spinner.
216 encryptionSpinner.adapter = encryptionArrayAdapter
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) {
223 // Hide the unneeded layout items.
224 encryptionPasswordTextInputLayout.visibility = View.GONE
225 openKeychainRequiredTextView.visibility = View.GONE
226 openKeychainImportInstructionsTextView.visibility = View.GONE
228 // Show the file location card.
229 fileLocationCardView.visibility = View.VISIBLE
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
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)
239 // Clear the file name edit text.
240 fileNameEditText.text.clear()
242 // Disable the import/export button.
243 importExportButton.isEnabled = false
246 PASSWORD_ENCRYPTION -> {
247 // Hide the OpenPGP layout items.
248 openKeychainRequiredTextView.visibility = View.GONE
249 openKeychainImportInstructionsTextView.visibility = View.GONE
251 // Show the password encryption layout items.
252 encryptionPasswordTextInputLayout.visibility = View.VISIBLE
254 // Show the file location card.
255 fileLocationCardView.visibility = View.VISIBLE
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
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)
265 // Clear the file name edit text.
266 fileNameEditText.text.clear()
268 // Disable the import/export button.
269 importExportButton.isEnabled = false
272 OPENPGP_ENCRYPTION -> {
273 // Hide the password encryption layout items.
274 encryptionPasswordTextInputLayout.visibility = View.GONE
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
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
287 // Set the text of the import button to be `Decrypt`.
288 importExportButton.setText(R.string.decrypt)
290 // Clear the file name edit text.
291 fileNameEditText.text.clear()
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
300 // Enable the export button.
301 importExportButton.isEnabled = true
303 } else { // OpenKeychain is not installed.
304 // Show the OpenPGP required layout item.
305 openKeychainRequiredTextView.visibility = View.VISIBLE
307 // Hide the file location card.
308 fileLocationCardView.visibility = View.GONE
314 override fun onNothingSelected(parent: AdapterView<*>?) {}
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) {
323 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
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()
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) {
339 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
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()
349 // Enable the export button if the file name is populated.
350 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
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)
373 fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
374 importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
378 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
379 // Run the default commands.
380 super.onSaveInstanceState(savedInstanceState)
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)
391 savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
392 savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
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
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
409 // Set the text on the import/export button to be `Import`.
410 importExportButton.setText(R.string.import_button)
413 // Display the file name views.
414 fileNameLinearLayout.visibility = View.VISIBLE
415 importExportButton.visibility = View.VISIBLE
417 // Clear the file name edit text.
418 fileNameEditText.text.clear()
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
426 // Set the text on the import/export button to be `Export`.
427 importExportButton.setText(R.string.export)
429 // Show the import/export button.
430 importExportButton.visibility = View.VISIBLE
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
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
443 // Clear the file name edit text.
444 fileNameEditText.text.clear()
446 // Disable the import/export button.
447 importExportButton.isEnabled = false
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, ImportExportDatabaseHelper.SCHEMA_VERSION))
461 else // Password encryption is selected.
462 browseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, ImportExportDatabaseHelper.SCHEMA_VERSION))
466 fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
467 // Instantiate the import export database helper.
468 val importExportDatabaseHelper = ImportExportDatabaseHelper()
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 = ""
475 // Get the file name string.
476 val fileNameString = fileNameEditText.text.toString()
478 // Import according to the encryption type.
479 when (encryptionSpinner.selectedItemPosition) {
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))!!
486 // Import the unencrypted file.
487 importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
489 // Close the input stream.
491 } catch (exception: FileNotFoundException) {
492 // Update the import status.
493 importStatus = exception.toString()
496 // Restart Privacy Browser if successful.
497 if (importStatus == ImportExportDatabaseHelper.IMPORT_SUCCESSFUL)
498 restartPrivacyBrowser()
501 PASSWORD_ENCRYPTION -> {
503 // Get the encryption password.
504 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
506 // Get an input stream for the file name.
507 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
509 // Initialize a salt byte array. Salt protects against rainbow table attacks.
510 val saltByteArray = ByteArray(32)
512 // Get the salt from the beginning of the import file.
513 inputStream.read(saltByteArray)
515 // Create an initialization vector.
516 val initializationVector = ByteArray(12)
518 // Get the initialization vector from the import file.
519 inputStream.read(initializationVector)
521 // Convert the encryption password to a byte array.
522 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
524 // Create an encryption password with salt byte array.
525 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
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)
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)
533 // Get a SHA-512 message digest.
534 val messageDigest = MessageDigest.getInstance("SHA-512")
536 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
537 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
539 // Truncate the encryption password byte array to 256 bits (32 bytes).
540 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
542 // Create an AES secret key from the encryption password byte array.
543 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
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")
548 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
549 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
551 // Initialize the cipher.
552 cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
554 // Create a cipher input stream.
555 val cipherInputStream = CipherInputStream(inputStream, cipher)
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)
561 // Create a private temporary unencrypted import file.
562 val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
564 // Create an temporary unencrypted import file output stream.
565 val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
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)
573 // Flush the temporary unencrypted import file output stream.
574 temporaryUnencryptedImportFileOutputStream.flush()
576 // Close the streams.
577 temporaryUnencryptedImportFileOutputStream.close()
578 cipherInputStream.close()
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())
590 // Create a temporary unencrypted import file input stream.
591 val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
593 // Import the temporary unencrypted import file.
594 importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
596 // Close the temporary unencrypted import file input stream.
597 temporaryUnencryptedImportFileInputStream.close()
599 // Delete the temporary unencrypted import file.
600 temporaryUnencryptedImportFile.delete()
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()
611 OPENPGP_ENCRYPTION -> {
613 // Get a handle for the file provider directory.
614 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
616 // Create the file provider directory. Any errors will be handled by the catch statement below.
617 fileProviderDirectory.mkdir()
619 // Set the temporary PGP encrypted import file.
620 temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
622 // Create a temporary PGP encrypted import file output stream.
623 val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
625 // Get an input stream for the file name.
626 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
628 // Create a transfer byte array.
629 val transferByteArray = ByteArray(1024)
631 // Create an integer to track the number of bytes read.
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)
638 // Flush the temporary PGP encrypted import file output stream.
639 temporaryPgpEncryptedImportFileOutputStream.flush()
641 // Close the streams.
643 temporaryPgpEncryptedImportFileOutputStream.close()
645 // Create a decryption intent for OpenKeychain.
646 val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
648 // Include the URI to be decrypted.
649 openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
651 // Allow OpenKeychain to read the file URI.
652 openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
654 // Send the intent to the OpenKeychain package.
655 openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
658 openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
660 // Update the import status.
661 importStatus = ImportExportDatabaseHelper.IMPORT_SUCCESSFUL
662 } catch (exception: Exception) {
663 // Update the import status.
664 importStatus = exception.toString()
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()
674 } else { // Export is selected.
675 // Export according to the encryption type.
676 when (encryptionSpinner.selectedItemPosition) {
678 // Get the file name string.
679 val noEncryptionFileNameString = fileNameEditText.text.toString()
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))!!
686 // Export the unencrypted file.
687 val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
689 // Close the output stream.
690 exportFileOutputStream.close()
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()
696 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 == ImportExportDatabaseHelper.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, ImportExportDatabaseHelper.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 != ImportExportDatabaseHelper.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)