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 packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
193 } catch (exception: PackageManager.NameNotFoundException) {
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)
210 // Create an array adapter for the spinner.
211 val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
213 // Set the drop down view resource on the spinner.
214 encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
216 // Set the array adapter for the spinner.
217 encryptionSpinner.adapter = encryptionArrayAdapter
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) {
224 // Hide the unneeded layout items.
225 encryptionPasswordTextInputLayout.visibility = View.GONE
226 openKeychainRequiredTextView.visibility = View.GONE
227 openKeychainImportInstructionsTextView.visibility = View.GONE
229 // Show the file location card.
230 fileLocationCardView.visibility = View.VISIBLE
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
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)
240 // Clear the file name edit text.
241 fileNameEditText.text.clear()
243 // Disable the import/export button.
244 importExportButton.isEnabled = false
247 PASSWORD_ENCRYPTION -> {
248 // Hide the OpenPGP layout items.
249 openKeychainRequiredTextView.visibility = View.GONE
250 openKeychainImportInstructionsTextView.visibility = View.GONE
252 // Show the password encryption layout items.
253 encryptionPasswordTextInputLayout.visibility = View.VISIBLE
255 // Show the file location card.
256 fileLocationCardView.visibility = View.VISIBLE
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
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)
266 // Clear the file name edit text.
267 fileNameEditText.text.clear()
269 // Disable the import/export button.
270 importExportButton.isEnabled = false
273 OPENPGP_ENCRYPTION -> {
274 // Hide the password encryption layout items.
275 encryptionPasswordTextInputLayout.visibility = View.GONE
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
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
288 // Set the text of the import button to be `Decrypt`.
289 importExportButton.setText(R.string.decrypt)
291 // Clear the file name edit text.
292 fileNameEditText.text.clear()
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
301 // Enable the export button.
302 importExportButton.isEnabled = true
304 } else { // OpenKeychain is not installed.
305 // Show the OpenPGP required layout item.
306 openKeychainRequiredTextView.visibility = View.VISIBLE
308 // Hide the file location card.
309 fileLocationCardView.visibility = View.GONE
315 override fun onNothingSelected(parent: AdapterView<*>?) {}
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) {
324 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
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()
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) {
340 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
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()
350 // Enable the export button if the file name is populated.
351 importExportButton.isEnabled = fileNameEditText.text.toString().isNotEmpty()
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)
374 fileNameEditText.post { fileNameEditText.setText(savedInstanceState.getString(FILE_NAME_TEXT)) }
375 importExportButton.text = savedInstanceState.getString(IMPORT_EXPORT_BUTTON_TEXT)
379 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
380 // Run the default commands.
381 super.onSaveInstanceState(savedInstanceState)
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)
392 savedInstanceState.putString(FILE_NAME_TEXT, fileNameEditText.text.toString())
393 savedInstanceState.putString(IMPORT_EXPORT_BUTTON_TEXT, importExportButton.text.toString())
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
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
410 // Set the text on the import/export button to be `Import`.
411 importExportButton.setText(R.string.import_button)
414 // Display the file name views.
415 fileNameLinearLayout.visibility = View.VISIBLE
416 importExportButton.visibility = View.VISIBLE
418 // Clear the file name edit text.
419 fileNameEditText.text.clear()
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
427 // Set the text on the import/export button to be `Export`.
428 importExportButton.setText(R.string.export)
430 // Show the import/export button.
431 importExportButton.visibility = View.VISIBLE
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
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
444 // Clear the file name edit text.
445 fileNameEditText.text.clear()
447 // Disable the import/export button.
448 importExportButton.isEnabled = false
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))
467 fun importExport(@Suppress("UNUSED_PARAMETER") view: View) {
468 // Instantiate the import export database helper.
469 val importExportDatabaseHelper = ImportExportDatabaseHelper()
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 = ""
476 // Get the file name string.
477 val fileNameString = fileNameEditText.text.toString()
479 // Import according to the encryption type.
480 when (encryptionSpinner.selectedItemPosition) {
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))!!
487 // Import the unencrypted file.
488 importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
490 // Close the input stream.
492 } catch (exception: FileNotFoundException) {
493 // Update the import status.
494 importStatus = exception.toString()
497 // Restart Privacy Browser if successful.
498 if (importStatus == IMPORT_SUCCESSFUL)
499 restartPrivacyBrowser()
502 PASSWORD_ENCRYPTION -> {
504 // Get the encryption password.
505 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
507 // Get an input stream for the file name.
508 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
510 // Initialize a salt byte array. Salt protects against rainbow table attacks.
511 val saltByteArray = ByteArray(32)
513 // Get the salt from the beginning of the import file.
514 inputStream.read(saltByteArray)
516 // Create an initialization vector.
517 val initializationVector = ByteArray(12)
519 // Get the initialization vector from the import file.
520 inputStream.read(initializationVector)
522 // Convert the encryption password to a byte array.
523 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
525 // Create an encryption password with salt byte array.
526 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
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)
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)
534 // Get a SHA-512 message digest.
535 val messageDigest = MessageDigest.getInstance("SHA-512")
537 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
538 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
540 // Truncate the encryption password byte array to 256 bits (32 bytes).
541 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
543 // Create an AES secret key from the encryption password byte array.
544 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
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")
549 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
550 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
552 // Initialize the cipher.
553 cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
555 // Create a cipher input stream.
556 val cipherInputStream = CipherInputStream(inputStream, cipher)
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)
562 // Create a private temporary unencrypted import file.
563 val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
565 // Create an temporary unencrypted import file output stream.
566 val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
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)
574 // Flush the temporary unencrypted import file output stream.
575 temporaryUnencryptedImportFileOutputStream.flush()
577 // Close the streams.
578 temporaryUnencryptedImportFileOutputStream.close()
579 cipherInputStream.close()
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())
591 // Create a temporary unencrypted import file input stream.
592 val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
594 // Import the temporary unencrypted import file.
595 importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
597 // Close the temporary unencrypted import file input stream.
598 temporaryUnencryptedImportFileInputStream.close()
600 // Delete the temporary unencrypted import file.
601 temporaryUnencryptedImportFile.delete()
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()
612 OPENPGP_ENCRYPTION -> {
614 // Get a handle for the file provider directory.
615 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
617 // Create the file provider directory. Any errors will be handled by the catch statement below.
618 fileProviderDirectory.mkdir()
620 // Set the temporary PGP encrypted import file.
621 temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
623 // Create a temporary PGP encrypted import file output stream.
624 val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
626 // Get an input stream for the file name.
627 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
629 // Create a transfer byte array.
630 val transferByteArray = ByteArray(1024)
632 // Create an integer to track the number of bytes read.
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)
639 // Flush the temporary PGP encrypted import file output stream.
640 temporaryPgpEncryptedImportFileOutputStream.flush()
642 // Close the streams.
644 temporaryPgpEncryptedImportFileOutputStream.close()
646 // Create a decryption intent for OpenKeychain.
647 val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
649 // Include the URI to be decrypted.
650 openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
652 // Allow OpenKeychain to read the file URI.
653 openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
655 // Send the intent to the OpenKeychain package.
656 openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
659 openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
661 // Update the import status.
662 importStatus = IMPORT_SUCCESSFUL
663 } catch (exception: Exception) {
664 // Update the import status.
665 importStatus = exception.toString()
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) {
677 // Get the file name string.
678 val noEncryptionFileNameString = fileNameEditText.text.toString()
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))!!
685 // Export the unencrypted file.
686 val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
688 // Close the output stream.
689 exportFileOutputStream.close()
691 // Display an export disposition snackbar.
692 if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
693 Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
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()
702 PASSWORD_ENCRYPTION -> {
704 // Create a temporary unencrypted export file.
705 val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
707 // Create a temporary unencrypted export output stream.
708 val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
710 // Populate the temporary unencrypted export.
711 val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
713 // Close the temporary unencrypted export output stream.
714 temporaryUnencryptedExportOutputStream.close()
716 // Create an unencrypted export file input stream.
717 val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
719 // Get the encryption password.
720 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
722 // Initialize a secure random number generator.
723 val secureRandom = SecureRandom()
725 // Initialize a salt byte array. Salt protects against rainbow table attacks.
726 val saltByteArray = ByteArray(32)
728 // Get a 256 bit (32 byte) random salt.
729 secureRandom.nextBytes(saltByteArray)
731 // Convert the encryption password to a byte array.
732 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
734 // Create an encryption password with salt byte array.
735 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
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)
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)
743 // Get a SHA-512 message digest.
744 val messageDigest = MessageDigest.getInstance("SHA-512")
746 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
747 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
749 // Truncate the encryption password byte array to 256 bits (32 bytes).
750 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
752 // Create an AES secret key from the encryption password byte array.
753 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
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)
758 // Populate the initialization vector with random data.
759 secureRandom.nextBytes(initializationVector)
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")
764 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
765 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
767 // Initialize the cipher.
768 cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
770 // Get the file name string.
771 val passwordEncryptionFileNameString = fileNameEditText.text.toString()
773 // Get the export file output stream.
774 val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString))!!
776 // Add the salt and the initialization vector to the export file output stream.
777 exportFileOutputStream.write(saltByteArray)
778 exportFileOutputStream.write(initializationVector)
780 // Create a cipher output stream.
781 val cipherOutputStream = CipherOutputStream(exportFileOutputStream, cipher)
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)
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)
792 // Close the streams.
793 cipherOutputStream.flush()
794 cipherOutputStream.close()
795 exportFileOutputStream.close()
796 unencryptedExportFileInputStream.close()
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())
807 // Delete the temporary unencrypted export file.
808 temporaryUnencryptedExportFile.delete()
810 // Display an export disposition snackbar.
811 if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
812 Snackbar.make(fileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
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()
821 OPENPGP_ENCRYPTION -> {
823 // Get a handle for the file provider directory.
824 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
826 // Create the file provider directory. Any errors will be handled by the catch statement below.
827 fileProviderDirectory.mkdir()
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))
833 // Delete the temporary pre-encrypted export file if it already exists.
834 if (temporaryPreEncryptedExportFile.exists())
835 temporaryPreEncryptedExportFile.delete()
837 // Create a temporary pre-encrypted export output stream.
838 val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
840 // Populate the temporary pre-encrypted export file.
841 val openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this)
843 // Flush the temporary pre-encryption export output stream.
844 temporaryPreEncryptedExportOutputStream.flush()
846 // Close the temporary pre-encryption export output stream.
847 temporaryPreEncryptedExportOutputStream.close()
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()
853 // Create an encryption intent for OpenKeychain.
854 val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
856 // Include the temporary unencrypted export file URI.
857 openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
859 // Allow OpenKeychain to read the file URI.
860 openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
862 // Send the intent to the OpenKeychain package.
863 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
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()
876 private fun restartPrivacyBrowser() {
877 // Create an intent to restart Privacy Browser.
878 val restartIntent = parentActivityIntent!!
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
883 // Create a restart handler.
884 val restartHandler = Handler(mainLooper)
886 // Create a restart runnable.
887 val restartRunnable = Runnable {
889 // Restart Privacy Browser.
890 startActivity(restartIntent)
892 // Kill this instance of Privacy Browser. Otherwise, the app exhibits sporadic behavior after the restart.
896 // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
897 restartHandler.postDelayed(restartRunnable, 150)