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:
parent
1877c6ab5a
commit
0f9540a645
6 changed files with 51 additions and 2 deletions
|
@ -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,
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })
|
||||||
}
|
}
|
||||||
|
|
|
@ -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") }
|
||||||
|
|
Loading…
Reference in a new issue