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.ScrollView
38 import android.widget.Spinner
39 import android.widget.TextView
41 import androidx.activity.result.contract.ActivityResultContracts
42 import androidx.appcompat.app.AppCompatActivity
43 import androidx.appcompat.widget.Toolbar
44 import androidx.cardview.widget.CardView
45 import androidx.core.content.FileProvider
46 import androidx.preference.PreferenceManager
48 import com.google.android.material.snackbar.Snackbar
49 import com.google.android.material.textfield.TextInputLayout
51 import com.stoutner.privacybrowser.BuildConfig
52 import com.stoutner.privacybrowser.R
53 import com.stoutner.privacybrowser.helpers.EXPORT_SUCCESSFUL
54 import com.stoutner.privacybrowser.helpers.IMPORT_EXPORT_SCHEMA_VERSION
55 import com.stoutner.privacybrowser.helpers.IMPORT_SUCCESSFUL
56 import com.stoutner.privacybrowser.helpers.ImportExportBookmarksHelper
57 import com.stoutner.privacybrowser.helpers.ImportExportDatabaseHelper
60 import java.io.FileInputStream
61 import java.io.FileNotFoundException
62 import java.io.FileOutputStream
63 import java.lang.Exception
64 import java.nio.charset.StandardCharsets
65 import java.security.MessageDigest
66 import java.security.SecureRandom
67 import java.util.Arrays
69 import javax.crypto.Cipher
70 import javax.crypto.CipherInputStream
71 import javax.crypto.CipherOutputStream
72 import javax.crypto.spec.GCMParameterSpec
73 import javax.crypto.spec.SecretKeySpec
74 import kotlin.system.exitProcess
76 // Define the encryption constants.
77 private const val NO_ENCRYPTION = 0
78 private const val PASSWORD_ENCRYPTION = 1
79 private const val OPENPGP_ENCRYPTION = 2
81 // Define the saved instance state constants.
82 private const val ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY = "A"
83 private const val OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY = "B"
84 private const val SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY = "C"
85 private const val SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "D"
86 private const val OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY = "E"
87 private const val SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY = "F"
88 private const val SETTINGS_FILE_NAME_TEXT = "G"
89 private const val SETTINGS_IMPORT_EXPORT_BUTTON_TEXT = "H"
90 private const val BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY = "I"
91 private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY = "J"
92 private const val BOOKMARKS_FILE_NAME_TEXT = "K"
93 private const val BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT = "L"
95 class ImportExportActivity : AppCompatActivity() {
96 // Define the class views.
97 private lateinit var scrollView: ScrollView
98 private lateinit var encryptionSpinner: Spinner
99 private lateinit var encryptionPasswordTextInputLayout: TextInputLayout
100 private lateinit var encryptionPasswordEditText: EditText
101 private lateinit var openKeychainRequiredTextView: TextView
102 private lateinit var settingsFileLocationCardView: CardView
103 private lateinit var settingsImportRadioButton: RadioButton
104 private lateinit var settingsFileNameLinearLayout: LinearLayout
105 private lateinit var settingsFileNameEditText: EditText
106 private lateinit var openKeychainImportInstructionsTextView: TextView
107 private lateinit var settingsImportExportButton: Button
108 private lateinit var bookmarksImportRadioButton: RadioButton
109 private lateinit var bookmarksFileNameLinearLayout: LinearLayout
110 private lateinit var bookmarksFileNameEditText: EditText
111 private lateinit var bookmarksImportExportButton: Button
113 // Define the class variables.
114 private lateinit var fileProviderDirectory: File
115 private var openKeychainInstalled = false
116 private lateinit var temporaryPgpEncryptedImportFile: File
117 private lateinit var temporaryPreEncryptedExportFile: File
119 // Define the result launchers. They must be defined before `onCreate()` is run or the app will crash.
120 private val settingsBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { 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 settings file name text.
127 settingsFileNameEditText.setText(fileNameString)
129 // Move the cursor to the end of the file name edit text.
130 settingsFileNameEditText.setSelection(fileNameString.length)
134 private val settingsBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
135 // Only do something if the user didn't press back from the file picker.
136 if (fileUri != null) {
137 // Get the file name string from the URI.
138 val fileNameString = fileUri.toString()
140 // Set the settings file name text.
141 settingsFileNameEditText.setText(fileNameString)
143 // Move the cursor to the end of the file name edit text.
144 settingsFileNameEditText.setSelection(fileNameString.length)
148 private val bookmarksBrowseForImportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { fileUri: Uri? ->
149 // Only do something if the user didn't press back from the file picker.
150 if (fileUri != null) {
151 // Get the file name string from the URI.
152 val fileNameString = fileUri.toString()
154 // Set the bookmarks file name text.
155 bookmarksFileNameEditText.setText(fileNameString)
157 // Move the cursor to the end of the file name edit text.
158 bookmarksFileNameEditText.setSelection(fileNameString.length)
160 // Scroll to the bottom.
162 scrollView.scrollY = scrollView.height
167 private val bookmarksBrowseForExportActivityResultLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("*/*")) { fileUri: Uri? ->
168 // Only do something if the user didn't press back from the file picker.
169 if (fileUri != null) {
170 // Get the file name string from the URI.
171 val fileNameString = fileUri.toString()
173 // Set the bookmarks file name text.
174 bookmarksFileNameEditText.setText(fileNameString)
176 // Move the cursor to the end of the file name edit text.
177 bookmarksFileNameEditText.setSelection(fileNameString.length)
179 // Scroll to the bottom.
181 scrollView.scrollY = scrollView.height
186 private val openKeychainDecryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
187 // Delete the temporary PGP encrypted import file.
188 if (temporaryPgpEncryptedImportFile.exists())
189 temporaryPgpEncryptedImportFile.delete()
191 // Delete the file provider directory if it exists.
192 if (fileProviderDirectory.exists())
193 fileProviderDirectory.delete()
196 private val openKeychainEncryptActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
197 // Delete the temporary pre-encrypted export file if it exists.
198 if (temporaryPreEncryptedExportFile.exists())
199 temporaryPreEncryptedExportFile.delete()
201 // Delete the file provider directory if it exists.
202 if (fileProviderDirectory.exists())
203 fileProviderDirectory.delete()
206 public override fun onCreate(savedInstanceState: Bundle?) {
207 // Get a handle for the shared preferences.
208 val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
210 // Get the preferences.
211 val allowScreenshots = sharedPreferences.getBoolean(getString(R.string.allow_screenshots_key), false)
212 val bottomAppBar = sharedPreferences.getBoolean(getString(R.string.bottom_app_bar_key), false)
214 // Disable screenshots if not allowed.
215 if (!allowScreenshots)
216 window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
218 // Run the default commands.
219 super.onCreate(savedInstanceState)
221 // Set the content view.
223 setContentView(R.layout.import_export_bottom_appbar)
225 setContentView(R.layout.import_export_top_appbar)
227 // Get a handle for the toolbar.
228 val toolbar = findViewById<Toolbar>(R.id.import_export_toolbar)
230 // Set the support action bar.
231 setSupportActionBar(toolbar)
233 // Get a handle for the action bar.
234 val actionBar = supportActionBar!!
236 // Display the home arrow on the support action bar.
237 actionBar.setDisplayHomeAsUpEnabled(true)
239 // Find out if OpenKeychain is installed.
240 openKeychainInstalled = try {
241 packageManager.getPackageInfo("org.sufficientlysecure.keychain", 0).versionName.isNotEmpty()
242 } catch (exception: PackageManager.NameNotFoundException) {
246 // Get handles for the views.
247 scrollView = findViewById(R.id.scrollview)
248 encryptionSpinner = findViewById(R.id.encryption_spinner)
249 encryptionPasswordTextInputLayout = findViewById(R.id.encryption_password_textinputlayout)
250 encryptionPasswordEditText = findViewById(R.id.encryption_password_edittext)
251 openKeychainRequiredTextView = findViewById(R.id.openkeychain_required_textview)
252 settingsFileLocationCardView = findViewById(R.id.settings_file_location_cardview)
253 settingsImportRadioButton = findViewById(R.id.settings_import_radiobutton)
254 val settingsExportRadioButton = findViewById<RadioButton>(R.id.settings_export_radiobutton)
255 settingsFileNameLinearLayout = findViewById(R.id.settings_file_name_linearlayout)
256 settingsFileNameEditText = findViewById(R.id.settings_file_name_edittext)
257 openKeychainImportInstructionsTextView = findViewById(R.id.openkeychain_import_instructions_textview)
258 settingsImportExportButton = findViewById(R.id.settings_import_export_button)
259 bookmarksImportRadioButton = findViewById(R.id.bookmarks_import_radiobutton)
260 bookmarksFileNameLinearLayout = findViewById(R.id.bookmarks_file_name_linearlayout)
261 bookmarksFileNameEditText = findViewById(R.id.bookmarks_file_name_edittext)
262 bookmarksImportExportButton = findViewById(R.id.bookmarks_import_export_button)
264 // Create an array adapter for the spinner.
265 val encryptionArrayAdapter = ArrayAdapter.createFromResource(this, R.array.encryption_type, R.layout.spinner_item)
267 // Set the drop down view resource on the spinner.
268 encryptionArrayAdapter.setDropDownViewResource(R.layout.spinner_dropdown_items)
270 // Set the array adapter for the spinner.
271 encryptionSpinner.adapter = encryptionArrayAdapter
273 // Update the UI when the spinner changes.
274 encryptionSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
275 override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
278 // Hide the unneeded layout items.
279 encryptionPasswordTextInputLayout.visibility = View.GONE
280 openKeychainRequiredTextView.visibility = View.GONE
281 openKeychainImportInstructionsTextView.visibility = View.GONE
283 // Show the file location card.
284 settingsFileLocationCardView.visibility = View.VISIBLE
286 // Show the file name linear layout if either import or export is checked.
287 if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
288 settingsFileNameLinearLayout.visibility = View.VISIBLE
290 // Reset the text of the import button, which may have been changed to `Decrypt`.
291 if (settingsImportRadioButton.isChecked)
292 settingsImportExportButton.setText(R.string.import_button)
294 // Clear the file name edit text.
295 settingsFileNameEditText.text.clear()
297 // Disable the import/export button.
298 settingsImportExportButton.isEnabled = false
301 PASSWORD_ENCRYPTION -> {
302 // Hide the OpenPGP layout items.
303 openKeychainRequiredTextView.visibility = View.GONE
304 openKeychainImportInstructionsTextView.visibility = View.GONE
306 // Show the password encryption layout items.
307 encryptionPasswordTextInputLayout.visibility = View.VISIBLE
309 // Show the file location card.
310 settingsFileLocationCardView.visibility = View.VISIBLE
312 // Show the file name linear layout if either import or export is checked.
313 if (settingsImportRadioButton.isChecked || settingsExportRadioButton.isChecked)
314 settingsFileNameLinearLayout.visibility = View.VISIBLE
316 // Reset the text of the import button, which may have been changed to `Decrypt`.
317 if (settingsImportRadioButton.isChecked)
318 settingsImportExportButton.setText(R.string.import_button)
320 // Clear the file name edit text.
321 settingsFileNameEditText.text.clear()
323 // Disable the import/export button.
324 settingsImportExportButton.isEnabled = false
327 OPENPGP_ENCRYPTION -> {
328 // Hide the password encryption layout items.
329 encryptionPasswordTextInputLayout.visibility = View.GONE
331 // Updated items based on the installation status of OpenKeychain.
332 if (openKeychainInstalled) { // OpenKeychain is installed.
333 // Show the file location card.
334 settingsFileLocationCardView.visibility = View.VISIBLE
336 // Update the layout based on the checked radio button.
337 if (settingsImportRadioButton.isChecked) {
338 // Show the file name linear layout and the OpenKeychain import instructions.
339 settingsFileNameLinearLayout.visibility = View.VISIBLE
340 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
342 // Set the text of the import button to be `Decrypt`.
343 settingsImportExportButton.setText(R.string.decrypt)
345 // Clear the file name edit text.
346 settingsFileNameEditText.text.clear()
348 // Disable the import/export button.
349 settingsImportExportButton.isEnabled = false
350 } else if (settingsExportRadioButton.isChecked) {
351 // Hide the file name linear layout and the OpenKeychain import instructions.
352 settingsFileNameLinearLayout.visibility = View.GONE
353 openKeychainImportInstructionsTextView.visibility = View.GONE
355 // Enable the export button.
356 settingsImportExportButton.isEnabled = true
358 } else { // OpenKeychain is not installed.
359 // Show the OpenPGP required layout item.
360 openKeychainRequiredTextView.visibility = View.VISIBLE
362 // Hide the file location card.
363 settingsFileLocationCardView.visibility = View.GONE
369 override fun onNothingSelected(parent: AdapterView<*>?) {}
372 // Update the status of the import/export button when the password changes.
373 encryptionPasswordEditText.addTextChangedListener(object : TextWatcher {
374 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
378 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
382 override fun afterTextChanged(s: Editable) {
383 // Enable the import/export button if both the file string and the password are populated.
384 settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
388 // Update the UI when the settings file name edit text changes.
389 settingsFileNameEditText.addTextChangedListener(object : TextWatcher {
390 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
394 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
398 override fun afterTextChanged(s: Editable) {
399 // Adjust the UI according to the encryption spinner position.
400 if (encryptionSpinner.selectedItemPosition == PASSWORD_ENCRYPTION) {
401 // Enable the settings import/export button if both the file name and the password are populated.
402 settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty() && encryptionPasswordEditText.text.toString().isNotEmpty()
404 // Enable the settings import/export button if the file name is populated.
405 settingsImportExportButton.isEnabled = settingsFileNameEditText.text.toString().isNotEmpty()
410 // Update the UI when the bookmarks file name edit text changes.
411 bookmarksFileNameEditText.addTextChangedListener(object : TextWatcher {
412 override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
416 override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
420 override fun afterTextChanged(s: Editable) {
421 // Enable the bookmarks import/export button if the file name is populated.
422 bookmarksImportExportButton.isEnabled = bookmarksFileNameEditText.text.toString().isNotEmpty()
426 // Check to see if the activity has been restarted.
427 if (savedInstanceState == null) { // The app has not been restarted.
428 // Initially hide the unneeded views.
429 encryptionPasswordTextInputLayout.visibility = View.GONE
430 openKeychainRequiredTextView.visibility = View.GONE
431 settingsFileNameLinearLayout.visibility = View.GONE
432 openKeychainImportInstructionsTextView.visibility = View.GONE
433 settingsImportExportButton.visibility = View.GONE
434 bookmarksFileNameLinearLayout.visibility = View.GONE
435 bookmarksImportExportButton.visibility = View.GONE
436 } else { // The app has been restarted.
437 // Restore the visibility of the views.
438 encryptionPasswordTextInputLayout.visibility = savedInstanceState.getInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY)
439 openKeychainRequiredTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY)
440 settingsFileLocationCardView.visibility = savedInstanceState.getInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY)
441 settingsFileNameLinearLayout.visibility = savedInstanceState.getInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
442 openKeychainImportInstructionsTextView.visibility = savedInstanceState.getInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY)
443 settingsImportExportButton.visibility = savedInstanceState.getInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY)
444 bookmarksFileNameLinearLayout.visibility = savedInstanceState.getInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY)
445 bookmarksImportExportButton.visibility = savedInstanceState.getInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY)
448 settingsFileNameEditText.post { settingsFileNameEditText.setText(savedInstanceState.getString(SETTINGS_FILE_NAME_TEXT)) }
449 settingsImportExportButton.text = savedInstanceState.getString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT)
450 bookmarksFileNameEditText.post { bookmarksFileNameEditText.setText(savedInstanceState.getString(BOOKMARKS_FILE_NAME_TEXT)) }
451 bookmarksImportExportButton.text = savedInstanceState.getString(BOOKMARKS_IMPORT_EXPORT_BUTTON_TEXT)
455 public override fun onSaveInstanceState(savedInstanceState: Bundle) {
456 // Run the default commands.
457 super.onSaveInstanceState(savedInstanceState)
459 // Save the visibility of the views.
460 savedInstanceState.putInt(ENCRYPTION_PASSWORD_TEXTINPUTLAYOUT_VISIBILITY, encryptionPasswordTextInputLayout.visibility)
461 savedInstanceState.putInt(OPEN_KEYCHAIN_REQUIRED_TEXTVIEW_VISIBILITY, openKeychainRequiredTextView.visibility)
462 savedInstanceState.putInt(SETTINGS_FILE_LOCATION_CARDVIEW_VISIBILITY, settingsFileLocationCardView.visibility)
463 savedInstanceState.putInt(SETTINGS_FILE_NAME_LINEARLAYOUT_VISIBILITY, settingsFileNameLinearLayout.visibility)
464 savedInstanceState.putInt(OPEN_KEYCHAIN_IMPORT_INSTRUCTIONS_TEXTVIEW_VISIBILITY, openKeychainImportInstructionsTextView.visibility)
465 savedInstanceState.putInt(SETTINGS_IMPORT_EXPORT_BUTTON_VISIBILITY, settingsImportExportButton.visibility)
466 savedInstanceState.putInt(BOOKMARKS_FILE_NAME_LINEARLAYOUT_VISIBILITY, bookmarksFileNameLinearLayout.visibility)
467 savedInstanceState.putInt(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.visibility)
470 savedInstanceState.putString(SETTINGS_FILE_NAME_TEXT, settingsFileNameEditText.text.toString())
471 savedInstanceState.putString(SETTINGS_IMPORT_EXPORT_BUTTON_TEXT, settingsImportExportButton.text.toString())
472 savedInstanceState.putString(BOOKMARKS_FILE_NAME_TEXT, bookmarksFileNameEditText.text.toString())
473 savedInstanceState.putString(BOOKMARKS_IMPORT_EXPORT_BUTTON_VISIBILITY, bookmarksImportExportButton.text.toString())
476 fun onClickBookmarksRadioButton(view: View) {
477 // Check to see if import or export was selected.
478 if (view.id == R.id.bookmarks_import_radiobutton) { // The bookmarks import radio button was selected.
479 // Set the text on the bookmarks import/export button to be `Import`.
480 bookmarksImportExportButton.setText(R.string.import_button)
481 } else { // The bookmarks export radio button was selected.
482 // Set the text on the bookmarks import/export button to be `Export`.
483 bookmarksImportExportButton.setText(R.string.export)
486 // Display the bookmarks views.
487 bookmarksFileNameLinearLayout.visibility = View.VISIBLE
488 bookmarksImportExportButton.visibility = View.VISIBLE
490 // Clear the bookmarks file name edit text.
491 bookmarksFileNameEditText.text.clear()
493 // Disable the bookmarks import/export button.
494 bookmarksImportExportButton.isEnabled = false
496 // Scroll to the bottom of the screen.
498 scrollView.scrollY = scrollView.height
502 fun onClickSettingsRadioButton(view: View) {
503 // Check to see if import or export was selected.
504 if (view.id == R.id.settings_import_radiobutton) { // The settings import radio button was selected.
505 // Check to see if OpenPGP encryption is selected.
506 if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption selected.
507 // Show the OpenKeychain import instructions.
508 openKeychainImportInstructionsTextView.visibility = View.VISIBLE
510 // Set the text on the settings import/export button to be `Decrypt`.
511 settingsImportExportButton.setText(R.string.decrypt)
512 } else { // OpenPGP encryption not selected.
513 // Hide the OpenKeychain import instructions.
514 openKeychainImportInstructionsTextView.visibility = View.GONE
516 // Set the text on the settings import/export button to be `Import`.
517 settingsImportExportButton.setText(R.string.import_button)
520 // Display the views.
521 settingsFileNameLinearLayout.visibility = View.VISIBLE
522 settingsImportExportButton.visibility = View.VISIBLE
524 // Clear the settings file name edit text.
525 settingsFileNameEditText.text.clear()
527 // Disable the settings import/export button.
528 settingsImportExportButton.isEnabled = false
529 } else { // The settings export radio button was selected.
530 // Hide the OpenKeychain import instructions.
531 openKeychainImportInstructionsTextView.visibility = View.GONE
533 // Set the text on the settings import/export button to be `Export`.
534 settingsImportExportButton.setText(R.string.export)
536 // Show the settings import/export button.
537 settingsImportExportButton.visibility = View.VISIBLE
539 // Check to see if OpenPGP encryption is selected.
540 if (encryptionSpinner.selectedItemPosition == OPENPGP_ENCRYPTION) { // OpenPGP encryption is selected.
541 // Hide the settings file name views.
542 settingsFileNameLinearLayout.visibility = View.GONE
544 // Enable the settings export button.
545 settingsImportExportButton.isEnabled = true
546 } else { // OpenPGP encryption is not selected.
547 // Show the settings file name view.
548 settingsFileNameLinearLayout.visibility = View.VISIBLE
550 // Clear the settings file name edit text.
551 settingsFileNameEditText.text.clear()
553 // Disable the settings import/export button.
554 settingsImportExportButton.isEnabled = false
559 fun settingsBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
560 // Check to see if import or export is selected.
561 if (settingsImportRadioButton.isChecked) { // Import is selected.
562 // Open the file picker.
563 settingsBrowseForImportActivityResultLauncher.launch("*/*")
564 } else { // Export is selected
565 // Open the file picker with the export name according to the encryption type.
566 if (encryptionSpinner.selectedItemPosition == NO_ENCRYPTION) // No encryption is selected.
567 settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
568 else // Password encryption is selected.
569 settingsBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_settings_pbs_aes, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
573 fun bookmarksBrowse(@Suppress("UNUSED_PARAMETER") view: View) {
574 // Check to see if import or export is selected.
575 if (bookmarksImportRadioButton.isChecked) { // Import is selected.
576 // Open the file picker.
577 bookmarksBrowseForImportActivityResultLauncher.launch("*/*")
578 } else { // Export is selected.
579 // Open the file picker.
580 bookmarksBrowseForExportActivityResultLauncher.launch(getString(R.string.privacy_browser_bookmarks_html))
584 fun importExportBookmarks(@Suppress("UNUSED_PARAMETER") view: View) {
585 // Instantiate the import/export bookmarks helper.
586 val importExportBookmarksHelper = ImportExportBookmarksHelper()
588 // Get the file name string.
589 val fileNameString = bookmarksFileNameEditText.text.toString()
591 // Check to see if import or export is selected.
592 if (bookmarksImportRadioButton.isChecked) { // Import is selected.
593 // Import the bookmarks.
594 importExportBookmarksHelper.importBookmarks(fileNameString, context = this, scrollView)
596 // Repopulate the bookmarks in the main WebView activity.
597 MainWebViewActivity.restartFromBookmarksActivity = true
598 } else { // Export is selected.
599 // Export the bookmarks.
600 importExportBookmarksHelper.exportBookmarks(fileNameString, context = this, scrollView)
604 fun importExportSettings(@Suppress("UNUSED_PARAMETER") view: View) {
605 // Instantiate the import/export database helper.
606 val importExportDatabaseHelper = ImportExportDatabaseHelper()
608 // Check to see if import or export is selected.
609 if (settingsImportRadioButton.isChecked) { // Import is selected.
610 // Initialize the import status string
611 var importStatus = ""
613 // Get the file name string.
614 val fileNameString = settingsFileNameEditText.text.toString()
616 // Import according to the encryption type.
617 when (encryptionSpinner.selectedItemPosition) {
620 // Get an input stream for the file name.
621 // A file may be opened directly once the minimum API >= 29. <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
622 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
624 // Import the unencrypted file.
625 importStatus = importExportDatabaseHelper.importUnencrypted(inputStream, this)
627 // Close the input stream.
629 } catch (exception: FileNotFoundException) {
630 // Update the import status.
631 importStatus = exception.toString()
634 // Restart Privacy Browser if successful.
635 if (importStatus == IMPORT_SUCCESSFUL)
636 restartPrivacyBrowser()
639 PASSWORD_ENCRYPTION -> {
641 // Get the encryption password.
642 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
644 // Get an input stream for the file name.
645 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
647 // Initialize a salt byte array. Salt protects against rainbow table attacks.
648 val saltByteArray = ByteArray(32)
650 // Get the salt from the beginning of the import file.
651 inputStream.read(saltByteArray)
653 // Create an initialization vector.
654 val initializationVector = ByteArray(12)
656 // Get the initialization vector from the import file.
657 inputStream.read(initializationVector)
659 // Convert the encryption password to a byte array.
660 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
662 // Create an encryption password with salt byte array.
663 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
665 // Populate the first part of the encryption password with salt byte array with the encryption password.
666 System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
668 // Populate the second part of the encryption password with salt byte array with the salt.
669 System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
671 // Get a SHA-512 message digest.
672 val messageDigest = MessageDigest.getInstance("SHA-512")
674 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
675 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
677 // Truncate the encryption password byte array to 256 bits (32 bytes).
678 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
680 // Create an AES secret key from the encryption password byte array.
681 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
683 // 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.
684 val cipher = Cipher.getInstance("AES/GCM/NoPadding")
686 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
687 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
689 // Initialize the cipher.
690 cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
692 // Create a cipher input stream.
693 val cipherInputStream = CipherInputStream(inputStream, cipher)
695 // 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.
696 var numberOfBytesRead: Int
697 val decryptedBytes = ByteArray(16)
699 // Create a private temporary unencrypted import file.
700 val temporaryUnencryptedImportFile = File.createTempFile("temporary_unencrypted_import_file", null, applicationContext.cacheDir)
702 // Create an temporary unencrypted import file output stream.
703 val temporaryUnencryptedImportFileOutputStream = FileOutputStream(temporaryUnencryptedImportFile)
705 // 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.
706 while (cipherInputStream.read(decryptedBytes).also { numberOfBytesRead = it } != -1) {
707 // Write the data to the temporary unencrypted import file output stream.
708 temporaryUnencryptedImportFileOutputStream.write(decryptedBytes, 0, numberOfBytesRead)
711 // Flush the temporary unencrypted import file output stream.
712 temporaryUnencryptedImportFileOutputStream.flush()
714 // Close the streams.
715 temporaryUnencryptedImportFileOutputStream.close()
716 cipherInputStream.close()
719 // Wipe the encryption data from memory.
720 Arrays.fill(saltByteArray, 0.toByte())
721 Arrays.fill(initializationVector, 0.toByte())
722 Arrays.fill(encryptionPasswordByteArray, 0.toByte())
723 Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
724 Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
725 Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
726 Arrays.fill(decryptedBytes, 0.toByte())
728 // Create a temporary unencrypted import file input stream.
729 val temporaryUnencryptedImportFileInputStream = FileInputStream(temporaryUnencryptedImportFile)
731 // Import the temporary unencrypted import file.
732 importStatus = importExportDatabaseHelper.importUnencrypted(temporaryUnencryptedImportFileInputStream, this)
734 // Close the temporary unencrypted import file input stream.
735 temporaryUnencryptedImportFileInputStream.close()
737 // Delete the temporary unencrypted import file.
738 temporaryUnencryptedImportFile.delete()
740 // Restart Privacy Browser if successful.
741 if (importStatus == IMPORT_SUCCESSFUL)
742 restartPrivacyBrowser()
743 } catch (exception: Exception) {
744 // Update the import status.
745 importStatus = exception.toString()
749 OPENPGP_ENCRYPTION -> {
751 // Get a handle for the file provider directory.
752 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
754 // Create the file provider directory. Any errors will be handled by the catch statement below.
755 fileProviderDirectory.mkdir()
757 // Set the temporary PGP encrypted import file.
758 temporaryPgpEncryptedImportFile = File.createTempFile("temporary_pgp_encrypted_import_file", null, fileProviderDirectory)
760 // Create a temporary PGP encrypted import file output stream.
761 val temporaryPgpEncryptedImportFileOutputStream = FileOutputStream(temporaryPgpEncryptedImportFile)
763 // Get an input stream for the file name.
764 val inputStream = contentResolver.openInputStream(Uri.parse(fileNameString))!!
766 // Create a transfer byte array.
767 val transferByteArray = ByteArray(1024)
769 // Create an integer to track the number of bytes read.
772 // Copy the input stream to the temporary PGP encrypted import file.
773 while (inputStream.read(transferByteArray).also { bytesRead = it } > 0)
774 temporaryPgpEncryptedImportFileOutputStream.write(transferByteArray, 0, bytesRead)
776 // Flush the temporary PGP encrypted import file output stream.
777 temporaryPgpEncryptedImportFileOutputStream.flush()
779 // Close the streams.
781 temporaryPgpEncryptedImportFileOutputStream.close()
783 // Create a decryption intent for OpenKeychain.
784 val openKeychainDecryptIntent = Intent("org.sufficientlysecure.keychain.action.DECRYPT_DATA")
786 // Include the URI to be decrypted.
787 openKeychainDecryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPgpEncryptedImportFile)
789 // Allow OpenKeychain to read the file URI.
790 openKeychainDecryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
792 // Send the intent to the OpenKeychain package.
793 openKeychainDecryptIntent.setPackage("org.sufficientlysecure.keychain")
796 openKeychainDecryptActivityResultLauncher.launch(openKeychainDecryptIntent)
798 // Update the import status.
799 importStatus = IMPORT_SUCCESSFUL
800 } catch (exception: Exception) {
801 // Update the import status.
802 importStatus = exception.toString()
807 // Display a snack bar with the import error if it was unsuccessful.
808 if (importStatus != IMPORT_SUCCESSFUL)
809 Snackbar.make(settingsFileNameEditText, getString(R.string.import_failed, importStatus), Snackbar.LENGTH_INDEFINITE).show()
810 } else { // Export is selected.
811 // Export according to the encryption type.
812 when (encryptionSpinner.selectedItemPosition) {
814 // Get the file name string.
815 val noEncryptionFileNameString = settingsFileNameEditText.text.toString()
818 // Get the export file output stream, truncating any existing content.
819 // A file may be opened directly once the minimum API >= 29. <https://developer.android.com/reference/kotlin/android/content/ContentResolver#openfile>
820 val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(noEncryptionFileNameString), "wt")!!
822 // Export the unencrypted file.
823 val noEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(exportFileOutputStream, this)
825 // Close the output stream.
826 exportFileOutputStream.close()
828 // Display an export disposition snackbar.
829 if (noEncryptionExportStatus == EXPORT_SUCCESSFUL)
830 Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
832 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, noEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
833 } catch (fileNotFoundException: FileNotFoundException) {
834 // Display a snackbar with the exception.
835 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, fileNotFoundException), Snackbar.LENGTH_INDEFINITE).show()
839 PASSWORD_ENCRYPTION -> {
841 // Create a temporary unencrypted export file.
842 val temporaryUnencryptedExportFile = File.createTempFile("temporary_unencrypted_export_file", null, applicationContext.cacheDir)
844 // Create a temporary unencrypted export output stream.
845 val temporaryUnencryptedExportOutputStream = FileOutputStream(temporaryUnencryptedExportFile)
847 // Populate the temporary unencrypted export.
848 val passwordEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryUnencryptedExportOutputStream, this)
850 // Close the temporary unencrypted export output stream.
851 temporaryUnencryptedExportOutputStream.close()
853 // Create an unencrypted export file input stream.
854 val unencryptedExportFileInputStream = FileInputStream(temporaryUnencryptedExportFile)
856 // Get the encryption password.
857 val encryptionPasswordString = encryptionPasswordEditText.text.toString()
859 // Initialize a secure random number generator.
860 val secureRandom = SecureRandom()
862 // Initialize a salt byte array. Salt protects against rainbow table attacks.
863 val saltByteArray = ByteArray(32)
865 // Get a 256 bit (32 byte) random salt.
866 secureRandom.nextBytes(saltByteArray)
868 // Convert the encryption password to a byte array.
869 val encryptionPasswordByteArray = encryptionPasswordString.toByteArray(StandardCharsets.UTF_8)
871 // Create an encryption password with salt byte array.
872 val encryptionPasswordWithSaltByteArray = ByteArray(encryptionPasswordByteArray.size + saltByteArray.size)
874 // Populate the first part of the encryption password with salt byte array with the encryption password.
875 System.arraycopy(encryptionPasswordByteArray, 0, encryptionPasswordWithSaltByteArray, 0, encryptionPasswordByteArray.size)
877 // Populate the second part of the encryption password with salt byte array with the salt.
878 System.arraycopy(saltByteArray, 0, encryptionPasswordWithSaltByteArray, encryptionPasswordByteArray.size, saltByteArray.size)
880 // Get a SHA-512 message digest.
881 val messageDigest = MessageDigest.getInstance("SHA-512")
883 // Hash the salted encryption password. Otherwise, any characters after the 32nd character in the password are ignored.
884 val hashedEncryptionPasswordWithSaltByteArray = messageDigest.digest(encryptionPasswordWithSaltByteArray)
886 // Truncate the encryption password byte array to 256 bits (32 bytes).
887 val truncatedHashedEncryptionPasswordWithSaltByteArray = Arrays.copyOf(hashedEncryptionPasswordWithSaltByteArray, 32)
889 // Create an AES secret key from the encryption password byte array.
890 val secretKey = SecretKeySpec(truncatedHashedEncryptionPasswordWithSaltByteArray, "AES")
892 // Create an initialization vector. According to NIST, a 12 byte initialization vector is more secure than a 16 byte one.
893 val initializationVector = ByteArray(12)
895 // Populate the initialization vector with random data.
896 secureRandom.nextBytes(initializationVector)
898 // 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.
899 val cipher = Cipher.getInstance("AES/GCM/NoPadding")
901 // Set the GCM tag length to be 128 bits (the maximum) and apply the initialization vector.
902 val gcmParameterSpec = GCMParameterSpec(128, initializationVector)
904 // Initialize the cipher.
905 cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec)
907 // Get the file name string.
908 val passwordEncryptionFileNameString = settingsFileNameEditText.text.toString()
910 // Get the export file output stream, truncating any existing content.
911 val exportFileOutputStream = contentResolver.openOutputStream(Uri.parse(passwordEncryptionFileNameString), "wt")!!
913 // Add the salt and the initialization vector to the export file output stream.
914 exportFileOutputStream.write(saltByteArray)
915 exportFileOutputStream.write(initializationVector)
917 // Create a cipher output stream.
918 val cipherOutputStream = CipherOutputStream(exportFileOutputStream, cipher)
920 // 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.
921 var numberOfBytesRead: Int
922 val encryptedBytes = ByteArray(16)
924 // 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.
925 while (unencryptedExportFileInputStream.read(encryptedBytes).also { numberOfBytesRead = it } != -1)
926 // Write the data to the cipher output stream.
927 cipherOutputStream.write(encryptedBytes, 0, numberOfBytesRead)
929 // Close the streams.
930 cipherOutputStream.flush()
931 cipherOutputStream.close()
932 exportFileOutputStream.close()
933 unencryptedExportFileInputStream.close()
935 // Wipe the encryption data from memory.
936 Arrays.fill(saltByteArray, 0.toByte())
937 Arrays.fill(encryptionPasswordByteArray, 0.toByte())
938 Arrays.fill(encryptionPasswordWithSaltByteArray, 0.toByte())
939 Arrays.fill(hashedEncryptionPasswordWithSaltByteArray, 0.toByte())
940 Arrays.fill(truncatedHashedEncryptionPasswordWithSaltByteArray, 0.toByte())
941 Arrays.fill(initializationVector, 0.toByte())
942 Arrays.fill(encryptedBytes, 0.toByte())
944 // Delete the temporary unencrypted export file.
945 temporaryUnencryptedExportFile.delete()
947 // Display an export disposition snackbar.
948 if (passwordEncryptionExportStatus == EXPORT_SUCCESSFUL)
949 Snackbar.make(settingsFileNameEditText, getString(R.string.export_successful), Snackbar.LENGTH_SHORT).show()
951 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, passwordEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
952 } catch (exception: Exception) {
953 // Display a snackbar with the exception.
954 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
958 OPENPGP_ENCRYPTION -> {
960 // Get a handle for the file provider directory.
961 fileProviderDirectory = File(applicationContext.cacheDir.toString() + "/" + getString(R.string.file_provider_directory))
963 // Create the file provider directory. Any errors will be handled by the catch statement below.
964 fileProviderDirectory.mkdir()
966 // Set the temporary pre-encrypted export file.
967 temporaryPreEncryptedExportFile = File(fileProviderDirectory.toString() + "/" +
968 getString(R.string.privacy_browser_settings_pbs, BuildConfig.VERSION_NAME, IMPORT_EXPORT_SCHEMA_VERSION))
970 // Delete the temporary pre-encrypted export file if it already exists.
971 if (temporaryPreEncryptedExportFile.exists())
972 temporaryPreEncryptedExportFile.delete()
974 // Create a temporary pre-encrypted export output stream.
975 val temporaryPreEncryptedExportOutputStream = FileOutputStream(temporaryPreEncryptedExportFile)
977 // Populate the temporary pre-encrypted export file.
978 val openpgpEncryptionExportStatus = importExportDatabaseHelper.exportUnencrypted(temporaryPreEncryptedExportOutputStream, this)
980 // Flush the temporary pre-encryption export output stream.
981 temporaryPreEncryptedExportOutputStream.flush()
983 // Close the temporary pre-encryption export output stream.
984 temporaryPreEncryptedExportOutputStream.close()
986 // Display an export error snackbar if the temporary pre-encrypted export failed.
987 if (openpgpEncryptionExportStatus != EXPORT_SUCCESSFUL)
988 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, openpgpEncryptionExportStatus), Snackbar.LENGTH_INDEFINITE).show()
990 // Create an encryption intent for OpenKeychain.
991 val openKeychainEncryptIntent = Intent("org.sufficientlysecure.keychain.action.ENCRYPT_DATA")
993 // Include the temporary unencrypted export file URI.
994 openKeychainEncryptIntent.data = FileProvider.getUriForFile(this, getString(R.string.file_provider), temporaryPreEncryptedExportFile)
996 // Allow OpenKeychain to read the file URI.
997 openKeychainEncryptIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
999 // Send the intent to the OpenKeychain package.
1000 openKeychainEncryptIntent.setPackage("org.sufficientlysecure.keychain")
1003 openKeychainEncryptActivityResultLauncher.launch(openKeychainEncryptIntent)
1004 } catch (exception: Exception) {
1005 // Display a snackbar with the exception.
1006 Snackbar.make(settingsFileNameEditText, getString(R.string.export_failed, exception), Snackbar.LENGTH_INDEFINITE).show()
1013 private fun restartPrivacyBrowser() {
1014 // Create an intent to restart Privacy Browser.
1015 val restartIntent = parentActivityIntent!!
1017 // `Intent.FLAG_ACTIVITY_CLEAR_TASK` removes all activities from the stack. It requires `Intent.FLAG_ACTIVITY_NEW_TASK`.
1018 restartIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
1020 // Create a restart handler.
1021 val restartHandler = Handler(mainLooper)
1023 // Create a restart runnable.
1024 val restartRunnable = Runnable {
1026 // Restart Privacy Browser.
1027 startActivity(restartIntent)
1029 // Kill this instance of Privacy Browser. Otherwise, the app exhibits sporadic behavior after the restart.
1033 // Restart Privacy Browser after 150 milliseconds to allow enough time for the preferences to be saved.
1034 restartHandler.postDelayed(restartRunnable, 150)