mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-31 22:35:17 +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 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,
|
||||||
|
@@ -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,17 +100,19 @@ 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() }
|
||||||
val dialog = PasswordDialog()
|
requireKeysExist {
|
||||||
lifecycleScope.launch {
|
val dialog = PasswordDialog()
|
||||||
withContext(Dispatchers.Main) {
|
lifecycleScope.launch {
|
||||||
dialog.password.collectLatest { value ->
|
withContext(Dispatchers.Main) {
|
||||||
if (value != null) {
|
dialog.password.collectLatest { value ->
|
||||||
decrypt(File(filePath), clientState, action, value)
|
if (value != null) {
|
||||||
|
decrypt(File(filePath), clientState, action, value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
|
||||||
}
|
}
|
||||||
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun decrypt(
|
private suspend fun decrypt(
|
||||||
|
@@ -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
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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)
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
||||||
|
Reference in New Issue
Block a user