mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-30 22:05:19 +00:00
feat: offer to import a PGP key when none are present
This commit is contained in:
@@ -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,
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -369,4 +369,6 @@
|
||||
<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="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>
|
||||
|
Reference in New Issue
Block a user