aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2023-05-08 02:51:02 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2023-05-08 03:09:26 +0530
commitd988bdd0dcfe5235cedcbd6b6dc5a49e6c3df571 (patch)
treea3e29792be6088e1c2052fcedded7093b75446bf /app/src/main
parent4ff0525e951472bc187961a89dc97e79aba8c68e (diff)
feat: wire up passphrase cache
Currently has horrible UX and is behind an experimental feature flag
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt7
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt50
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt76
-rw-r--r--app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt5
-rw-r--r--app/src/main/java/app/passwordstore/util/features/Feature.kt3
-rw-r--r--app/src/main/res/values/strings.xml1
6 files changed, 124 insertions, 18 deletions
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 f3dff15e..ea1d9c76 100644
--- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
+++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
@@ -18,7 +18,6 @@ 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
import javax.inject.Inject
@@ -41,9 +40,10 @@ constructor(
suspend fun decrypt(
password: String,
+ identities: List<GpgIdentifier>,
message: ByteArrayInputStream,
out: ByteArrayOutputStream,
- ) = withContext(dispatcherProvider.io()) { decryptPgp(password, message, out) }
+ ) = withContext(dispatcherProvider.io()) { decryptPgp(password, identities, message, out) }
suspend fun encrypt(
identities: List<GpgIdentifier>,
@@ -53,11 +53,12 @@ constructor(
private suspend fun decryptPgp(
password: String,
+ identities: List<GpgIdentifier>,
message: ByteArrayInputStream,
out: ByteArrayOutputStream,
): Result<Unit, CryptoHandlerException> {
+ val keys = identities.map { id -> pgpKeyManager.getKeyById(id) }.getAll()
val decryptionOptions = PGPDecryptOptions.Builder().build()
- val keys = pgpKeyManager.getAllKeys().unwrap()
return pgpCryptoHandler.decrypt(keys, password, message, out, decryptionOptions)
}
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 4e8c9891..fe400238 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -13,13 +13,17 @@ import android.os.Bundle
import android.view.autofill.AutofillManager
import androidx.annotation.RequiresApi
import androidx.lifecycle.lifecycleScope
+import app.passwordstore.data.crypto.GPGPassphraseCache
import app.passwordstore.data.passfile.PasswordEntry
import app.passwordstore.ui.crypto.BasePgpActivity
import app.passwordstore.ui.crypto.PasswordDialog
+import app.passwordstore.util.auth.BiometricAuthenticator
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.autofill.AutofillResponseBuilder
import app.passwordstore.util.autofill.DirectoryStructure
import app.passwordstore.util.extensions.asLog
+import app.passwordstore.util.features.Feature.EnableGPGPassphraseCache
+import app.passwordstore.util.features.Features
import com.github.androidpasswordstore.autofillparser.AutofillAction
import com.github.androidpasswordstore.autofillparser.Credentials
import com.github.michaelbull.result.getOrElse
@@ -77,6 +81,8 @@ class AutofillDecryptActivity : BasePgpActivity() {
}
@Inject lateinit var passwordEntryFactory: PasswordEntry.Factory
+ @Inject lateinit var features: Features
+ @Inject lateinit var passphraseCache: GPGPassphraseCache
private lateinit var directoryStructure: DirectoryStructure
@@ -101,18 +107,46 @@ class AutofillDecryptActivity : BasePgpActivity() {
directoryStructure = AutofillPreferences.directoryStructure(this)
logcat { action.toString() }
requireKeysExist {
- val dialog = PasswordDialog()
- lifecycleScope.launch {
- withContext(Dispatchers.Main) {
- dialog.password.collectLatest { value ->
- if (value != null) {
- decrypt(File(filePath), clientState, action, value)
+ val gpgIdentifiers = getGpgIdentifiers("") ?: return@requireKeysExist
+ if (
+ BiometricAuthenticator.canAuthenticate(this) && features.isEnabled(EnableGPGPassphraseCache)
+ ) {
+ BiometricAuthenticator.authenticate(this) { authResult ->
+ if (authResult is BiometricAuthenticator.Result.Success) {
+ lifecycleScope.launch {
+ val cachedPassphrase =
+ passphraseCache.retrieveCachedPassphrase(
+ this@AutofillDecryptActivity,
+ gpgIdentifiers.first()
+ )
+ if (cachedPassphrase != null) {
+ decrypt(File(filePath), clientState, action, cachedPassphrase)
+ } else {
+ askPassphrase(filePath, clientState, action)
+ }
}
+ } else {
+ askPassphrase(filePath, clientState, action)
+ }
+ }
+ } else {
+ askPassphrase(filePath, clientState, action)
+ }
+ }
+ }
+
+ private fun askPassphrase(filePath: String, clientState: Bundle, action: AutofillAction) {
+ 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(
@@ -143,6 +177,7 @@ class AutofillDecryptActivity : BasePgpActivity() {
}
private suspend fun decryptCredential(file: File, password: String): Credentials? {
+ val gpgIdentifiers = getGpgIdentifiers("") ?: return null
runCatching { file.readBytes().inputStream() }
.onFailure { e ->
logcat(ERROR) { e.asLog("File to decrypt not found") }
@@ -154,6 +189,7 @@ class AutofillDecryptActivity : BasePgpActivity() {
val outputStream = ByteArrayOutputStream()
repository.decrypt(
password,
+ gpgIdentifiers,
encryptedInput,
outputStream,
)
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 3df2422f..05d1edeb 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
@@ -11,13 +11,18 @@ import android.view.Menu
import android.view.MenuItem
import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
+import app.passwordstore.crypto.GpgIdentifier
+import app.passwordstore.data.crypto.GPGPassphraseCache
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.auth.BiometricAuthenticator
import app.passwordstore.util.extensions.getString
import app.passwordstore.util.extensions.unsafeLazy
import app.passwordstore.util.extensions.viewBinding
+import app.passwordstore.util.features.Feature.EnableGPGPassphraseCache
+import app.passwordstore.util.features.Features
import app.passwordstore.util.settings.Constants
import app.passwordstore.util.settings.PreferenceKeys
import com.github.michaelbull.result.Err
@@ -28,7 +33,6 @@ import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
-import kotlin.time.ExperimentalTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
@@ -39,14 +43,14 @@ import kotlinx.coroutines.withContext
import logcat.LogPriority.ERROR
import logcat.logcat
-@OptIn(ExperimentalTime::class)
@AndroidEntryPoint
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 passphraseCache: GPGPassphraseCache
+ @Inject lateinit var features: Features
private var passwordEntry: PasswordEntry? = null
private var retries = 0
@@ -63,7 +67,16 @@ class DecryptActivity : BasePgpActivity() {
true
}
}
- requireKeysExist { askPassphrase(isError = false) }
+ if (
+ BiometricAuthenticator.canAuthenticate(this@DecryptActivity) &&
+ features.isEnabled(EnableGPGPassphraseCache)
+ ) {
+ BiometricAuthenticator.authenticate(this@DecryptActivity) { authResult ->
+ requireKeysExist { decrypt(isError = false, authResult) }
+ }
+ } else {
+ requireKeysExist { decrypt(isError = false, BiometricAuthenticator.Result.Cancelled) }
+ }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -134,7 +147,28 @@ class DecryptActivity : BasePgpActivity() {
)
}
- private fun askPassphrase(isError: Boolean) {
+ private fun decrypt(isError: Boolean, authResult: BiometricAuthenticator.Result) {
+ val gpgIdentifiers = getGpgIdentifiers("") ?: return
+ lifecycleScope.launch(dispatcherProvider.main()) {
+ if (authResult is BiometricAuthenticator.Result.Success) {
+ val cachedPassphrase =
+ passphraseCache.retrieveCachedPassphrase(this@DecryptActivity, gpgIdentifiers.first())
+ if (cachedPassphrase != null) {
+ decryptWithCachedPassphrase(cachedPassphrase, gpgIdentifiers, authResult)
+ } else {
+ askPassphrase(isError, gpgIdentifiers, authResult)
+ }
+ } else {
+ askPassphrase(isError, gpgIdentifiers, authResult)
+ }
+ }
+ }
+
+ private fun askPassphrase(
+ isError: Boolean,
+ gpgIdentifiers: List<GpgIdentifier>,
+ authResult: BiometricAuthenticator.Result,
+ ) {
if (retries < MAX_RETRIES) {
retries += 1
} else {
@@ -147,16 +181,19 @@ class DecryptActivity : BasePgpActivity() {
lifecycleScope.launch(dispatcherProvider.main()) {
dialog.password.collectLatest { value ->
if (value != null) {
- when (val result = decryptWithPassphrase(value)) {
+ when (val result = decryptWithPassphrase(value, gpgIdentifiers)) {
is Ok -> {
val entry = passwordEntryFactory.create(result.value.toByteArray())
passwordEntry = entry
createPasswordUI(entry)
startAutoDismissTimer()
+ if (authResult is BiometricAuthenticator.Result.Success) {
+ passphraseCache.cachePassphrase(this@DecryptActivity, gpgIdentifiers.first(), value)
+ }
}
is Err -> {
logcat(ERROR) { result.error.stackTraceToString() }
- askPassphrase(isError = true)
+ askPassphrase(isError = true, gpgIdentifiers, authResult)
}
}
}
@@ -165,12 +202,35 @@ class DecryptActivity : BasePgpActivity() {
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
}
- private suspend fun decryptWithPassphrase(password: String) = runCatching {
+ private suspend fun decryptWithCachedPassphrase(
+ passphrase: String,
+ identifiers: List<GpgIdentifier>,
+ authResult: BiometricAuthenticator.Result,
+ ) {
+ when (val result = decryptWithPassphrase(passphrase, identifiers)) {
+ is Ok -> {
+ val entry = passwordEntryFactory.create(result.value.toByteArray())
+ passwordEntry = entry
+ createPasswordUI(entry)
+ startAutoDismissTimer()
+ }
+ is Err -> {
+ logcat(ERROR) { result.error.stackTraceToString() }
+ decrypt(isError = true, authResult = authResult)
+ }
+ }
+ }
+
+ private suspend fun decryptWithPassphrase(
+ password: String,
+ gpgIdentifiers: List<GpgIdentifier>,
+ ) = runCatching {
val message = withContext(dispatcherProvider.io()) { File(fullPath).readBytes().inputStream() }
val outputStream = ByteArrayOutputStream()
val result =
repository.decrypt(
password,
+ gpgIdentifiers,
message,
outputStream,
)
diff --git a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt
index a704b7a4..7be4a268 100644
--- a/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt
+++ b/app/src/main/java/app/passwordstore/ui/settings/PGPSettings.kt
@@ -9,6 +9,7 @@ import androidx.fragment.app.FragmentActivity
import app.passwordstore.R
import app.passwordstore.ui.pgp.PGPKeyListActivity
import app.passwordstore.util.extensions.launchActivity
+import app.passwordstore.util.features.Feature
import app.passwordstore.util.settings.PreferenceKeys
import de.Maxr1998.modernpreferences.PreferenceScreen
import de.Maxr1998.modernpreferences.helpers.onClick
@@ -31,6 +32,10 @@ class PGPSettings(private val activity: FragmentActivity) : SettingsProvider {
titleRes = R.string.pref_pgp_ascii_armor_title
persistent = true
}
+ switch(Feature.EnableGPGPassphraseCache.configKey) {
+ titleRes = R.string.pref_title_passphrase_cache
+ defaultValue = false
+ }
}
}
}
diff --git a/app/src/main/java/app/passwordstore/util/features/Feature.kt b/app/src/main/java/app/passwordstore/util/features/Feature.kt
index a714587a..a0729f4d 100644
--- a/app/src/main/java/app/passwordstore/util/features/Feature.kt
+++ b/app/src/main/java/app/passwordstore/util/features/Feature.kt
@@ -22,6 +22,9 @@ enum class Feature(
/** Opt into the new SSH layer implemented as a freestanding module. */
EnableNewSSHLayer(false, "enable_new_ssh"),
+
+ /** Opt into a cache layer for GPG passphrases. */
+ EnableGPGPassphraseCache(false, "enable_gpg_passphrase_cache"),
;
companion object {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f3e2dbd0..e0a1d3b7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -371,4 +371,5 @@
<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>
+ <string name="pref_title_passphrase_cache">Enable passphrase caching</string>
</resources>