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
This commit is contained in:
Harsh Shandilya 2024-05-27 20:59:20 +05:30 committed by GitHub
parent 1877c6ab5a
commit 0f9540a645
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 51 additions and 2 deletions

View file

@ -45,6 +45,11 @@ constructor(
out: ByteArrayOutputStream, out: ByteArrayOutputStream,
) = withContext(dispatcherProvider.io()) { decryptPgp(password, identities, message, out) } ) = 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( suspend fun encrypt(
identities: List<PGPIdentifier>, identities: List<PGPIdentifier>,
content: ByteArrayInputStream, content: ByteArrayInputStream,

View file

@ -131,12 +131,16 @@ class AutofillDecryptActivity : BasePGPActivity() {
} }
} }
private fun askPassphrase( private suspend fun askPassphrase(
filePath: String, filePath: String,
identifiers: List<PGPIdentifier>, identifiers: List<PGPIdentifier>,
clientState: Bundle, clientState: Bundle,
action: AutofillAction, action: AutofillAction,
) { ) {
if (!repository.isPasswordProtected(identifiers)) {
decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
return
}
val dialog = PasswordDialog() val dialog = PasswordDialog()
dialog.show(supportFragmentManager, "PASSWORD_DIALOG") dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle -> dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle ->

View file

@ -179,7 +179,7 @@ class DecryptActivity : BasePGPActivity() {
} }
} }
private fun askPassphrase( private suspend fun askPassphrase(
isError: Boolean, isError: Boolean,
gpgIdentifiers: List<PGPIdentifier>, gpgIdentifiers: List<PGPIdentifier>,
authResult: BiometricResult, authResult: BiometricResult,
@ -189,6 +189,10 @@ class DecryptActivity : BasePGPActivity() {
} else { } else {
finish() finish()
} }
if (!repository.isPasswordProtected(gpgIdentifiers)) {
decryptWithPassphrase(passphrase = "", gpgIdentifiers, authResult)
return
}
val dialog = PasswordDialog() val dialog = PasswordDialog()
if (isError) { if (isError) {
dialog.setError() dialog.setError()

View file

@ -41,4 +41,9 @@ public interface CryptoHandler<Key, EncOpts : CryptoOptions, DecryptOpts : Crypt
/** Given a [fileName], return whether this instance can handle it. */ /** Given a [fileName], return whether this instance can handle it. */
public fun canHandle(fileName: String): Boolean 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
} }

View file

@ -11,6 +11,7 @@ import app.passwordstore.crypto.errors.NoKeysProvidedException
import app.passwordstore.crypto.errors.NonStandardAEAD import app.passwordstore.crypto.errors.NonStandardAEAD
import app.passwordstore.crypto.errors.UnknownError import app.passwordstore.crypto.errors.UnknownError
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import com.github.michaelbull.result.mapBoth
import com.github.michaelbull.result.mapError import com.github.michaelbull.result.mapError
import com.github.michaelbull.result.runCatching import com.github.michaelbull.result.runCatching
import java.io.InputStream import java.io.InputStream
@ -140,4 +141,14 @@ public class PGPainlessCryptoHandler @Inject constructor() :
public override fun canHandle(fileName: String): Boolean { public override fun canHandle(fileName: String): Boolean {
return fileName.substringAfterLast('.', "") == "gpg" 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 })
} }

View file

@ -155,6 +155,26 @@ class PGPainlessCryptoHandlerTest {
assertIs<NonStandardAEAD>(res.error, message = "${res.error.cause}") assertIs<NonStandardAEAD>(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 @Test
fun canHandleFiltersFormats() { fun canHandleFiltersFormats() {
assertFalse { cryptoHandler.canHandle("example.com") } assertFalse { cryptoHandler.canHandle("example.com") }