From 8af09d5bc8ce0286e88edb49428ada5b1fd89bd0 Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Sat, 25 Mar 2023 12:34:25 +0530 Subject: feat: offer to import a PGP key when none are present --- .../passwordstore/data/crypto/CryptoRepository.kt | 7 ++++ .../ui/autofill/AutofillDecryptActivity.kt | 22 +++++------ .../app/passwordstore/ui/crypto/BasePgpActivity.kt | 46 +++++++++++++++++++++- .../app/passwordstore/ui/crypto/DecryptActivity.kt | 6 +-- .../ui/crypto/PasswordCreationActivity.kt | 6 +-- app/src/main/res/values/strings.xml | 2 + 6 files changed, 68 insertions(+), 21 deletions(-) (limited to 'app/src/main') diff --git a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt index fae13f21..f3dff15e 100644 --- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt @@ -17,6 +17,7 @@ import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.settings.PreferenceKeys import com.github.michaelbull.result.Result import com.github.michaelbull.result.getAll +import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.unwrap import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @@ -32,6 +33,12 @@ constructor( @SettingsPreferences private val settings: SharedPreferences, ) { + suspend fun hasKeys(): Boolean { + return withContext(dispatcherProvider.io()) { + pgpKeyManager.getAllKeys().mapBoth(success = { it.isNotEmpty() }, failure = { false }) + } + } + suspend fun decrypt( password: String, message: ByteArrayInputStream, diff --git a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt index e8e1f565..4e8c9891 100644 --- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt @@ -12,10 +12,9 @@ import android.os.Build import android.os.Bundle import android.view.autofill.AutofillManager import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.passfile.PasswordEntry +import app.passwordstore.ui.crypto.BasePgpActivity import app.passwordstore.ui.crypto.PasswordDialog import app.passwordstore.util.autofill.AutofillPreferences import app.passwordstore.util.autofill.AutofillResponseBuilder @@ -40,7 +39,7 @@ import logcat.logcat @RequiresApi(Build.VERSION_CODES.O) @AndroidEntryPoint -class AutofillDecryptActivity : AppCompatActivity() { +class AutofillDecryptActivity : BasePgpActivity() { companion object { @@ -78,7 +77,6 @@ class AutofillDecryptActivity : AppCompatActivity() { } @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository private lateinit var directoryStructure: DirectoryStructure @@ -102,17 +100,19 @@ class AutofillDecryptActivity : AppCompatActivity() { val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match directoryStructure = AutofillPreferences.directoryStructure(this) logcat { action.toString() } - val dialog = PasswordDialog() - lifecycleScope.launch { - withContext(Dispatchers.Main) { - dialog.password.collectLatest { value -> - if (value != null) { - decrypt(File(filePath), clientState, action, value) + requireKeysExist { + val dialog = PasswordDialog() + lifecycleScope.launch { + withContext(Dispatchers.Main) { + dialog.password.collectLatest { value -> + if (value != null) { + decrypt(File(filePath), clientState, action, value) + } } } } + dialog.show(supportFragmentManager, "PASSWORD_DIALOG") } - dialog.show(supportFragmentManager, "PASSWORD_DIALOG") } private suspend fun decrypt( diff --git a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt index f2778e2d..14f6c843 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/BasePgpActivity.kt @@ -12,11 +12,16 @@ import android.os.Build import android.os.Bundle import android.os.PersistableBundle import android.view.WindowManager +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import androidx.annotation.CallSuper import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import app.passwordstore.R +import app.passwordstore.data.crypto.CryptoRepository 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.getString 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.settings.Constants import app.passwordstore.util.settings.PreferenceKeys +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import java.io.File import javax.inject.Inject +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext @Suppress("Registered") @AndroidEntryPoint @@ -46,8 +54,22 @@ open class BasePgpActivity : AppCompatActivity() { */ 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 */ @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 @@ -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 * 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?) { copyTextToClipboard(password) - val clearAfter = settings.getString(PreferenceKeys.GENERAL_SHOW_TIME)?.toIntOrNull() ?: Constants.DEFAULT_DECRYPTION_TIMEOUT diff --git a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt index ce14c64d..3df2422f 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt @@ -11,12 +11,10 @@ import android.view.Menu import android.view.MenuItem import androidx.lifecycle.lifecycleScope import app.passwordstore.R -import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.password.FieldItem import app.passwordstore.databinding.DecryptLayoutBinding import app.passwordstore.ui.adapters.FieldItemAdapter -import app.passwordstore.util.coroutines.DispatcherProvider import app.passwordstore.util.extensions.getString import app.passwordstore.util.extensions.unsafeLazy import app.passwordstore.util.extensions.viewBinding @@ -48,8 +46,6 @@ class DecryptActivity : BasePgpActivity() { private val binding by viewBinding(DecryptLayoutBinding::inflate) private val relativeParentPath by unsafeLazy { getParentPath(fullPath, repoPath) } @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository - @Inject lateinit var dispatcherProvider: DispatcherProvider private var passwordEntry: PasswordEntry? = null private var retries = 0 @@ -67,7 +63,7 @@ class DecryptActivity : BasePgpActivity() { true } } - askPassphrase(isError = false) + requireKeysExist { askPassphrase(isError = false) } } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt index 9466daf2..1d32151f 100644 --- a/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/crypto/PasswordCreationActivity.kt @@ -25,7 +25,6 @@ import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.lifecycleScope import app.passwordstore.R import app.passwordstore.crypto.GpgIdentifier -import app.passwordstore.data.crypto.CryptoRepository import app.passwordstore.data.passfile.PasswordEntry import app.passwordstore.data.repo.PasswordRepository import app.passwordstore.databinding.PasswordCreationActivityBinding @@ -71,7 +70,6 @@ class PasswordCreationActivity : BasePgpActivity() { private val binding by viewBinding(PasswordCreationActivityBinding::inflate) @Inject lateinit var passwordEntryFactory: PasswordEntry.Factory - @Inject lateinit var repository: CryptoRepository private val suggestedName by unsafeLazy { intent.getStringExtra(EXTRA_FILE_NAME) } private val suggestedPass by unsafeLazy { intent.getStringExtra(EXTRA_PASSWORD) } @@ -268,11 +266,11 @@ class PasswordCreationActivity : BasePgpActivity() { } R.id.save_password -> { copy = false - encrypt() + requireKeysExist { encrypt() } } R.id.save_and_copy_password -> { copy = true - encrypt() + requireKeysExist { encrypt() } } else -> return super.onOptionsItemSelected(item) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b28814f..c5569929 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,4 +369,6 @@ Delete key? Remote branch name Import a key using the add button below + No keys imported + There are no PGP keys imported in the app yet, press the button below to pick a key file -- cgit v1.2.3