aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2024-05-27 20:59:20 +0530
committerGitHub <noreply@github.com>2024-05-27 15:29:20 +0000
commit0f9540a645ef66f3cf67294f75ba2c5d9d80078e (patch)
tree1f6e94f6565de5ee147b1fdcd694343cc6004d20
parent1877c6ab5a987e08797b13e4dc619294d03d4c02 (diff)
feat(pgpainless): add detection for passphrase-less messages (#3069)
* WIP: feat(pgpainless): add detection for passphrase-less messages * refactor: test keys instead of the message This makes more logical sense
-rw-r--r--app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt5
-rw-r--r--app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt6
-rw-r--r--app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt6
-rw-r--r--crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt5
-rw-r--r--crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt11
-rw-r--r--crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt20
6 files changed, 51 insertions, 2 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 50d7a841..773a355a 100644
--- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
+++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt
@@ -45,6 +45,11 @@ constructor(
out: ByteArrayOutputStream,
) = withContext(dispatcherProvider.io()) { decryptPgp(password, identities, message, out) }
+ suspend fun isPasswordProtected(identifiers: List<PGPIdentifier>): Boolean {
+ val keys = identifiers.map { pgpKeyManager.getKeyById(it) }.filterValues()
+ return pgpCryptoHandler.isPassphraseProtected(keys)
+ }
+
suspend fun encrypt(
identities: List<PGPIdentifier>,
content: 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 1af46a89..4518e62f 100644
--- a/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt
@@ -131,12 +131,16 @@ class AutofillDecryptActivity : BasePGPActivity() {
}
}
- private fun askPassphrase(
+ private suspend fun askPassphrase(
filePath: String,
identifiers: List<PGPIdentifier>,
clientState: Bundle,
action: AutofillAction,
) {
+ if (!repository.isPasswordProtected(identifiers)) {
+ decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
+ return
+ }
val dialog = PasswordDialog()
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle ->
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 b222f2ea..6f14bdea 100644
--- a/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
+++ b/app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt
@@ -179,7 +179,7 @@ class DecryptActivity : BasePGPActivity() {
}
}
- private fun askPassphrase(
+ private suspend fun askPassphrase(
isError: Boolean,
gpgIdentifiers: List<PGPIdentifier>,
authResult: BiometricResult,
@@ -189,6 +189,10 @@ class DecryptActivity : BasePGPActivity() {
} else {
finish()
}
+ if (!repository.isPasswordProtected(gpgIdentifiers)) {
+ decryptWithPassphrase(passphrase = "", gpgIdentifiers, authResult)
+ return
+ }
val dialog = PasswordDialog()
if (isError) {
dialog.setError()
diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
index 898cf058..c823342b 100644
--- a/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
+++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt
@@ -41,4 +41,9 @@ public interface CryptoHandler<Key, EncOpts : CryptoOptions, DecryptOpts : Crypt
/** Given a [fileName], return whether this instance can handle it. */
public fun canHandle(fileName: String): Boolean
+
+ /**
+ * Inspects the given [keys] and returns `false` if none of them require a passphrase to decrypt.
+ */
+ public fun isPassphraseProtected(keys: List<Key>): Boolean
}
diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
index 1aabe54f..b3f2a64b 100644
--- a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
+++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt
@@ -11,6 +11,7 @@ import app.passwordstore.crypto.errors.NoKeysProvidedException
import app.passwordstore.crypto.errors.NonStandardAEAD
import app.passwordstore.crypto.errors.UnknownError
import com.github.michaelbull.result.Result
+import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.runCatching
import java.io.InputStream
@@ -140,4 +141,14 @@ public class PGPainlessCryptoHandler @Inject constructor() :
public override fun canHandle(fileName: String): Boolean {
return fileName.substringAfterLast('.', "") == "gpg"
}
+
+ public override fun isPassphraseProtected(keys: List<PGPKey>): Boolean =
+ keys
+ .mapNotNull { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) }
+ .map(::keyringHasPassphrase)
+ .all { it }
+
+ internal fun keyringHasPassphrase(keyRing: PGPSecretKeyRing) =
+ runCatching { keyRing.secretKey.extractPrivateKey(null) }
+ .mapBoth(success = { false }, failure = { true })
}
diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
index 5de2bf4f..600cc39d 100644
--- a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
+++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt
@@ -156,6 +156,26 @@ class PGPainlessCryptoHandlerTest {
}
@Test
+ fun detectsKeysWithPassphrase() {
+ assertTrue(cryptoHandler.isPassphraseProtected(listOf(PGPKey(TestUtils.getArmoredSecretKey()))))
+ assertTrue(
+ cryptoHandler.isPassphraseProtected(
+ listOf(PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities()))
+ )
+ )
+ }
+
+ @Test
+ fun detectsKeysWithoutPassphrase() {
+ // Uses the internal method instead of the public API because GnuPG seems to have made it
+ // impossible to generate a key without a passphrase and I can't care to find a magical
+ // incantation to convince it I am smarter than whatever they are protecting against.
+ assertFalse(
+ cryptoHandler.keyringHasPassphrase(PGPainless.generateKeyRing().modernKeyRing("John Doe"))
+ )
+ }
+
+ @Test
fun canHandleFiltersFormats() {
assertFalse { cryptoHandler.canHandle("example.com") }
assertTrue { cryptoHandler.canHandle("example.com.gpg") }