Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 0f9540a

Browse files
authored
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
1 parent 1877c6a commit 0f9540a

File tree

6 files changed

+51
-2
lines changed

6 files changed

+51
-2
lines changed

app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ constructor(
4545
out: ByteArrayOutputStream,
4646
) = withContext(dispatcherProvider.io()) { decryptPgp(password, identities, message, out) }
4747

48+
suspend fun isPasswordProtected(identifiers: List<PGPIdentifier>): Boolean {
49+
val keys = identifiers.map { pgpKeyManager.getKeyById(it) }.filterValues()
50+
return pgpCryptoHandler.isPassphraseProtected(keys)
51+
}
52+
4853
suspend fun encrypt(
4954
identities: List<PGPIdentifier>,
5055
content: ByteArrayInputStream,

app/src/main/java/app/passwordstore/ui/autofill/AutofillDecryptActivity.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,12 +131,16 @@ class AutofillDecryptActivity : BasePGPActivity() {
131131
}
132132
}
133133

134-
private fun askPassphrase(
134+
private suspend fun askPassphrase(
135135
filePath: String,
136136
identifiers: List<PGPIdentifier>,
137137
clientState: Bundle,
138138
action: AutofillAction,
139139
) {
140+
if (!repository.isPasswordProtected(identifiers)) {
141+
decryptWithPassphrase(File(filePath), identifiers, clientState, action, password = "")
142+
return
143+
}
140144
val dialog = PasswordDialog()
141145
dialog.show(supportFragmentManager, "PASSWORD_DIALOG")
142146
dialog.setFragmentResultListener(PasswordDialog.PASSWORD_RESULT_KEY) { key, bundle ->

app/src/main/java/app/passwordstore/ui/crypto/DecryptActivity.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ class DecryptActivity : BasePGPActivity() {
179179
}
180180
}
181181

182-
private fun askPassphrase(
182+
private suspend fun askPassphrase(
183183
isError: Boolean,
184184
gpgIdentifiers: List<PGPIdentifier>,
185185
authResult: BiometricResult,
@@ -189,6 +189,10 @@ class DecryptActivity : BasePGPActivity() {
189189
} else {
190190
finish()
191191
}
192+
if (!repository.isPasswordProtected(gpgIdentifiers)) {
193+
decryptWithPassphrase(passphrase = "", gpgIdentifiers, authResult)
194+
return
195+
}
192196
val dialog = PasswordDialog()
193197
if (isError) {
194198
dialog.setError()

crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,9 @@ public interface CryptoHandler<Key, EncOpts : CryptoOptions, DecryptOpts : Crypt
4141

4242
/** Given a [fileName], return whether this instance can handle it. */
4343
public fun canHandle(fileName: String): Boolean
44+
45+
/**
46+
* Inspects the given [keys] and returns `false` if none of them require a passphrase to decrypt.
47+
*/
48+
public fun isPassphraseProtected(keys: List<Key>): Boolean
4449
}

crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import app.passwordstore.crypto.errors.NoKeysProvidedException
1111
import app.passwordstore.crypto.errors.NonStandardAEAD
1212
import app.passwordstore.crypto.errors.UnknownError
1313
import com.github.michaelbull.result.Result
14+
import com.github.michaelbull.result.mapBoth
1415
import com.github.michaelbull.result.mapError
1516
import com.github.michaelbull.result.runCatching
1617
import java.io.InputStream
@@ -140,4 +141,14 @@ public class PGPainlessCryptoHandler @Inject constructor() :
140141
public override fun canHandle(fileName: String): Boolean {
141142
return fileName.substringAfterLast('.', "") == "gpg"
142143
}
144+
145+
public override fun isPassphraseProtected(keys: List<PGPKey>): Boolean =
146+
keys
147+
.mapNotNull { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) }
148+
.map(::keyringHasPassphrase)
149+
.all { it }
150+
151+
internal fun keyringHasPassphrase(keyRing: PGPSecretKeyRing) =
152+
runCatching { keyRing.secretKey.extractPrivateKey(null) }
153+
.mapBoth(success = { false }, failure = { true })
143154
}

crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,26 @@ class PGPainlessCryptoHandlerTest {
155155
assertIs<NonStandardAEAD>(res.error, message = "${res.error.cause}")
156156
}
157157

158+
@Test
159+
fun detectsKeysWithPassphrase() {
160+
assertTrue(cryptoHandler.isPassphraseProtected(listOf(PGPKey(TestUtils.getArmoredSecretKey()))))
161+
assertTrue(
162+
cryptoHandler.isPassphraseProtected(
163+
listOf(PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities()))
164+
)
165+
)
166+
}
167+
168+
@Test
169+
fun detectsKeysWithoutPassphrase() {
170+
// Uses the internal method instead of the public API because GnuPG seems to have made it
171+
// impossible to generate a key without a passphrase and I can't care to find a magical
172+
// incantation to convince it I am smarter than whatever they are protecting against.
173+
assertFalse(
174+
cryptoHandler.keyringHasPassphrase(PGPainless.generateKeyRing().modernKeyRing("John Doe"))
175+
)
176+
}
177+
158178
@Test
159179
fun canHandleFiltersFormats() {
160180
assertFalse { cryptoHandler.canHandle("example.com") }

0 commit comments

Comments
 (0)