From 0f9540a645ef66f3cf67294f75ba2c5d9d80078e Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 27 May 2024 20:59:20 +0530 Subject: [PATCH] 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 --- .../data/crypto/CryptoRepository.kt | 5 +++++ .../ui/autofill/AutofillDecryptActivity.kt | 6 +++++- .../ui/crypto/DecryptActivity.kt | 6 +++++- .../app/passwordstore/crypto/CryptoHandler.kt | 5 +++++ .../crypto/PGPainlessCryptoHandler.kt | 11 ++++++++++ .../crypto/PGPainlessCryptoHandlerTest.kt | 20 +++++++++++++++++++ 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): Boolean { + val keys = identifiers.map { pgpKeyManager.getKeyById(it) }.filterValues() + return pgpCryptoHandler.isPassphraseProtected(keys) + } + suspend fun encrypt( identities: List, 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, 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, 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): 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): 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 @@ -155,6 +155,26 @@ class PGPainlessCryptoHandlerTest { assertIs(res.error, message = "${res.error.cause}") } + @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") }