feat: offer to import a PGP key when none are present

This commit is contained in:
Harsh Shandilya
2023-03-25 12:34:25 +05:30
parent 1e74012656
commit 8af09d5bc8
6 changed files with 68 additions and 21 deletions

View File

@@ -17,6 +17,7 @@ import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.settings.PreferenceKeys import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.getAll import com.github.michaelbull.result.getAll
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.unwrap import com.github.michaelbull.result.unwrap
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
@@ -32,6 +33,12 @@ constructor(
@SettingsPreferences private val settings: SharedPreferences, @SettingsPreferences private val settings: SharedPreferences,
) { ) {
suspend fun hasKeys(): Boolean {
return withContext(dispatcherProvider.io()) {
pgpKeyManager.getAllKeys().mapBoth(success = { it.isNotEmpty() }, failure = { false })
}
}
suspend fun decrypt( suspend fun decrypt(
password: String, password: String,
message: ByteArrayInputStream, message: ByteArrayInputStream,

View File

@@ -12,10 +12,9 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.autofill.AutofillManager import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.ui.crypto.BasePgpActivity
import app.passwordstore.ui.crypto.PasswordDialog import app.passwordstore.ui.crypto.PasswordDialog
import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.autofill.AutofillResponseBuilder import app.passwordstore.util.autofill.AutofillResponseBuilder
@@ -40,7 +39,7 @@ import logcat.logcat
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
@AndroidEntryPoint @AndroidEntryPoint
class AutofillDecryptActivity : AppCompatActivity() { class AutofillDecryptActivity : BasePgpActivity() {
companion object { companion object {
@@ -78,7 +77,6 @@ class AutofillDecryptActivity : AppCompatActivity() {
} }
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
@Inject lateinit var repository: CryptoRepository
private lateinit var directoryStructure: DirectoryStructure private lateinit var directoryStructure: DirectoryStructure
@@ -102,6 +100,7 @@ class AutofillDecryptActivity : AppCompatActivity() {
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
directoryStructure = AutofillPreferences.directoryStructure(this) directoryStructure = AutofillPreferences.directoryStructure(this)
logcat { action.toString() } logcat { action.toString() }
requireKeysExist {
val dialog = PasswordDialog() val dialog = PasswordDialog()
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -114,6 +113,7 @@ class AutofillDecryptActivity : AppCompatActivity() {
} }
dialog.show(supportFragmentManager, "PASSWORD_DIALOG") dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
} }
}
private suspend fun decrypt( private suspend fun decrypt(
filePath: File, filePath: File,

View File

@@ -12,11 +12,16 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.PersistableBundle import android.os.PersistableBundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import app.passwordstore.R import app.passwordstore.R
import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.injection.prefs.SettingsPreferences
import app.passwordstore.ui.pgp.PGPKeyImportActivity
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.clipboard import app.passwordstore.util.extensions.clipboard
import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.snackbar import app.passwordstore.util.extensions.snackbar
@@ -24,10 +29,13 @@ import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.services.ClipboardService import app.passwordstore.util.services.ClipboardService
import app.passwordstore.util.settings.Constants import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys import app.passwordstore.util.settings.PreferenceKeys
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Suppress("Registered") @Suppress("Registered")
@AndroidEntryPoint @AndroidEntryPoint
@@ -46,8 +54,22 @@ open class BasePgpActivity : AppCompatActivity() {
*/ */
val name: String by unsafeLazy { File(fullPath).nameWithoutExtension } val name: String by unsafeLazy { File(fullPath).nameWithoutExtension }
/** Action to invoke if [keyImportAction] succeeds. */
var onKeyImport: (() -> Unit)? = null
private val keyImportAction =
registerForActivityResult(StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
onKeyImport?.invoke()
onKeyImport = null
} else {
finish()
}
}
/** [SharedPreferences] instance used by subclasses to persist settings */ /** [SharedPreferences] instance used by subclasses to persist settings */
@SettingsPreferences @Inject lateinit var settings: SharedPreferences @SettingsPreferences @Inject lateinit var settings: SharedPreferences
@Inject lateinit var repository: CryptoRepository
@Inject lateinit var dispatcherProvider: DispatcherProvider
/** /**
* [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or * [onCreate] sets the window up with the right flags to prevent auth leaks through screenshots or
@@ -80,6 +102,29 @@ open class BasePgpActivity : AppCompatActivity() {
} }
} }
/**
* Function to execute [onKeysExist] only if there are PGP keys imported in the app's key manager.
*/
fun requireKeysExist(onKeysExist: () -> Unit) {
lifecycleScope.launch {
val hasKeys = repository.hasKeys()
if (!hasKeys) {
withContext(dispatcherProvider.main()) {
MaterialAlertDialogBuilder(this@BasePgpActivity)
.setTitle(resources.getString(R.string.no_keys_imported_dialog_title))
.setMessage(resources.getString(R.string.no_keys_imported_dialog_message))
.setPositiveButton(resources.getString(R.string.button_label_import)) { _, _ ->
onKeyImport = onKeysExist
keyImportAction.launch(Intent(this@BasePgpActivity, PGPKeyImportActivity::class.java))
}
.show()
}
} else {
onKeysExist()
}
}
}
/** /**
* Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to hide * Copies a provided [password] string to the clipboard. This wraps [copyTextToClipboard] to hide
* the default [Snackbar] and starts off an instance of [ClipboardService] to provide a way of * the default [Snackbar] and starts off an instance of [ClipboardService] to provide a way of
@@ -87,7 +132,6 @@ open class BasePgpActivity : AppCompatActivity() {
*/ */
fun copyPasswordToClipboard(password: String?) { fun copyPasswordToClipboard(password: String?) {
copyTextToClipboard(password) copyTextToClipboard(password)
val clearAfter = val clearAfter =
settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull()
?: Constants.DEFAULT_DECRYPTION_TIMEOUT ?: Constants.DEFAULT_DECRYPTION_TIMEOUT

View File

@@ -11,12 +11,10 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import app.passwordstore.R import app.passwordstore.R
import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.data.password.FieldItem import app.passwordstore.data.password.FieldItem
import app.passwordstore.databinding.DecryptLayoutBinding import app.passwordstore.databinding.DecryptLayoutBinding
import app.passwordstore.ui.adapters.FieldItemAdapter import app.passwordstore.ui.adapters.FieldItemAdapter
import app.passwordstore.util.coroutines.DispatcherProvider
import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.extensions.viewBinding import app.passwordstore.util.extensions.viewBinding
@@ -48,8 +46,6 @@ class DecryptActivity : BasePgpActivity() {
private val binding by viewBinding(DecryptLayoutBinding::inflate) private val binding by viewBinding(DecryptLayoutBinding::inflate)
private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) }
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
@Inject lateinit var repository: CryptoRepository
@Inject lateinit var dispatcherProvider: DispatcherProvider
private var passwordEntry: PasswordEntry? = null private var passwordEntry: PasswordEntry? = null
private var retries = 0 private var retries = 0
@@ -67,7 +63,7 @@ class DecryptActivity : BasePgpActivity() {
true true
} }
} }
askPassphrase(isError = false) requireKeysExist { askPassphrase(isError = false) }
} }
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {

View File

@@ -25,7 +25,6 @@ import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import app.passwordstore.R import app.passwordstore.R
import app.passwordstore.crypto.GpgIdentifier import app.passwordstore.crypto.GpgIdentifier
import app.passwordstore.data.crypto.CryptoRepository
import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.databinding.PasswordCreationActivityBinding import app.passwordstore.databinding.PasswordCreationActivityBinding
@@ -71,7 +70,6 @@ class PasswordCreationActivity : BasePgpActivity() {
private val binding by viewBinding(PasswordCreationActivityBinding::inflate) private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
@Inject lateinit var repository: CryptoRepository
private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) }
private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) }
@@ -268,11 +266,11 @@ class PasswordCreationActivity : BasePgpActivity() {
} }
R.id.save_password -> { R.id.save_password -> {
copy = false copy = false
encrypt() requireKeysExist { encrypt() }
} }
R.id.save_and_copy_password -> { R.id.save_and_copy_password -> {
copy = true copy = true
encrypt() requireKeysExist { encrypt() }
} }
else -> return super.onOptionsItemSelected(item) else -> return super.onOptionsItemSelected(item)
} }

View File

@@ -369,4 +369,6 @@
<string name="pgp_key_manager_delete_confirmation_dialog_title">Delete key?</string> <string name="pgp_key_manager_delete_confirmation_dialog_title">Delete key?</string>
<string name="git_utils_reset_remote_branch_title">Remote branch name</string> <string name="git_utils_reset_remote_branch_title">Remote branch name</string>
<string name="pgp_key_manager_no_keys_guidance">Import a key using the add button below</string> <string name="pgp_key_manager_no_keys_guidance">Import a key using the add button below</string>
<string name="no_keys_imported_dialog_title">No keys imported</string>
<string name="no_keys_imported_dialog_message">There are no PGP keys imported in the app yet, press the button below to pick a key file</string>
</resources> </resources>