Skip to content

Commit f89c7d4

Browse files
authored
Issue 1100 decrypt atts with kotlin (#1225)
* Added an ability to decrypt attachments with PGPainless. Removed 3Mb restriction for encrypted attachments. Refactored code.| #1100 * Added PgpDecrypt.| #1100 * Added handling some errors.| #1100 * Modified PgpDecrypt.decrypt() to return OpenPgpMetadata as result.| #1100 * Fixed wrongly added code.| #1100 * Improved streams usage in AttachmentDownloadManagerService.| #1100 * Reverted back changes in PgpMsg.| #1100 * Added PgpDecryptTest(not completed).| #1100 * Improved TemporaryFolder.createFileWithGivenSize to use really random data.| #1100 * Added PgpDecryptTest.testDecryptionErrorWrongPassphrase().| #1100 * Modified tests.| #1100 * Added using a regex pattern to detect encrypted attachments.| #1100 * Removed unused resources.| #1100 * Fixed "EOF" issue.| #1100 * Refactored code.| #1100 * Fixed PgpMsgTest after refactoring.| #1100
1 parent 13092d8 commit f89c7d4

File tree

10 files changed

+555
-155
lines changed

10 files changed

+555
-155
lines changed

FlowCrypt/src/main/java/com/flowcrypt/email/Constants.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,6 @@ class Constants {
7575
*/
7676
const val MAX_PUB_KEY_SIZE = 1024 * 256
7777

78-
/**
79-
* The max size off an attachment which can be decrypted via the app.
80-
*/
81-
const val MAX_ATTACHMENT_SIZE_WHICH_CAN_BE_DECRYPTED = 1024 * 1024 * 3
82-
8378
const val PGP_CACHE_DIR = "PGP"
8479
const val FORWARDED_ATTACHMENTS_CACHE_DIR = "forwarded"
8580
const val ATTACHMENTS_CACHE_DIR = "attachments"

FlowCrypt/src/main/java/com/flowcrypt/email/security/KeysStorageImpl.kt

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ import com.flowcrypt.email.database.entity.KeyEntity
1717
import com.flowcrypt.email.extensions.org.pgpainless.key.longId
1818
import com.flowcrypt.email.model.KeysStorage
1919
import com.flowcrypt.email.node.Node
20+
import com.flowcrypt.email.security.pgp.PgpDecrypt
2021
import com.flowcrypt.email.security.pgp.PgpKey
22+
import com.flowcrypt.email.util.exception.DecryptionException
2123
import org.bouncycastle.bcpg.ArmoredInputStream
24+
import org.bouncycastle.openpgp.PGPException
2225
import org.pgpainless.PGPainless
2326
import org.pgpainless.key.OpenPgpV4Fingerprint
2427
import org.pgpainless.key.protection.KeyRingProtectionSettings
@@ -42,31 +45,37 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
4245
private var keys = mutableListOf<KeyEntity>()
4346
private var nodeKeyDetailsList = mutableListOf<NodeKeyDetails>()
4447

45-
private val pureActiveAccountLiveData: LiveData<AccountEntity?> = Transformations.switchMap(nodeLiveData) {
46-
roomDatabase.accountDao().getActiveAccountLD()
47-
}
48+
private val pureActiveAccountLiveData: LiveData<AccountEntity?> =
49+
Transformations.switchMap(nodeLiveData) {
50+
roomDatabase.accountDao().getActiveAccountLD()
51+
}
4852

49-
private val encryptedKeysLiveData: LiveData<List<KeyEntity>> = Transformations.switchMap(pureActiveAccountLiveData) {
50-
roomDatabase.keysDao().getAllKeysByAccountLD(it?.email ?: "")
51-
}
53+
private val encryptedKeysLiveData: LiveData<List<KeyEntity>> =
54+
Transformations.switchMap(pureActiveAccountLiveData) {
55+
roomDatabase.keysDao().getAllKeysByAccountLD(it?.email ?: "")
56+
}
5257

5358
private val keysLiveData = encryptedKeysLiveData.switchMap { list ->
5459
liveData {
5560
emit(list.map {
5661
it.copy(
57-
privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(),
58-
passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase))
62+
privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(),
63+
passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase)
64+
)
5965
})
6066
}
6167
}
6268

63-
val nodeKeyDetailsLiveData: LiveData<List<NodeKeyDetails>> = Transformations.switchMap(keysLiveData) {
64-
liveData {
65-
emit(PgpKey.parseKeys(
66-
it.joinToString(separator = "\n") { keyEntity -> keyEntity.privateKeyAsString })
67-
.toNodeKeyDetailsList())
69+
val nodeKeyDetailsLiveData: LiveData<List<NodeKeyDetails>> =
70+
Transformations.switchMap(keysLiveData) {
71+
liveData {
72+
emit(
73+
PgpKey.parseKeys(
74+
it.joinToString(separator = "\n") { keyEntity -> keyEntity.privateKeyAsString })
75+
.toNodeKeyDetailsList()
76+
)
77+
}
6878
}
69-
}
7079

7180
init {
7281
keysLiveData.observeForever {
@@ -135,8 +144,11 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
135144
val key = getPgpPrivateKey(openPgpV4Fingerprint.longId)
136145
if (key != null) {
137146
val passphrase: Passphrase
138-
if (key.passphrase.isNullOrEmpty()) {
139-
passphrase = Passphrase.emptyPassphrase()
147+
if (key.passphrase == null) {
148+
throw DecryptionException(
149+
decryptionErrorType = PgpDecrypt.DecryptionErrorType.NEED_PASSPHRASE,
150+
e = PGPException("flowcrypt: need passphrase")
151+
)
140152
} else {
141153
passphrase = Passphrase.fromPassword(key.passphrase)
142154
}
@@ -161,20 +173,24 @@ class KeysStorageImpl private constructor(context: Context) : KeysStorage {
161173
*/
162174
suspend fun getLatestAllPgpPrivateKeys(): List<KeyEntity> {
163175
val account = pureActiveAccountLiveData.value
164-
?: roomDatabase.accountDao().getActiveAccountSuspend()
176+
?: roomDatabase.accountDao().getActiveAccountSuspend()
165177
account?.let { accountEntity ->
166178
val cachedKeysLongIds = keys.map { it.longId }.toSet()
167-
val latestEncryptedKeys = roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email)
168-
val latestKeysLongIds = roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email).map { it.longId }.toSet()
179+
val latestEncryptedKeys =
180+
roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email)
181+
val latestKeysLongIds =
182+
roomDatabase.keysDao().getAllKeysByAccountSuspend(accountEntity.email).map { it.longId }
183+
.toSet()
169184

170185
if (cachedKeysLongIds == latestKeysLongIds) {
171186
return keys
172187
}
173188

174189
return latestEncryptedKeys.map {
175190
it.copy(
176-
privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(),
177-
passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase))
191+
privateKey = KeyStoreCryptoManager.decryptSuspend(it.privateKeyAsString).toByteArray(),
192+
passphrase = KeyStoreCryptoManager.decryptSuspend(it.passphrase)
193+
)
178194
}
179195
}
180196

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
3+
* Contributors: DenBond7
4+
*/
5+
6+
package com.flowcrypt.email.security.pgp
7+
8+
import com.flowcrypt.email.util.exception.DecryptionException
9+
import org.bouncycastle.openpgp.PGPDataValidationException
10+
import org.bouncycastle.openpgp.PGPException
11+
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
12+
import org.pgpainless.PGPainless
13+
import org.pgpainless.decryption_verification.OpenPgpMetadata
14+
import org.pgpainless.exception.MessageNotIntegrityProtectedException
15+
import org.pgpainless.exception.ModificationDetectionException
16+
import org.pgpainless.key.protection.SecretKeyRingProtector
17+
import java.io.InputStream
18+
import java.io.OutputStream
19+
20+
/**
21+
* @author Denis Bondarenko
22+
* Date: 5/11/21
23+
* Time: 2:10 PM
24+
* E-mail: DenBond7@gmail.com
25+
*/
26+
object PgpDecrypt {
27+
val DETECT_SEPARATE_ENCRYPTED_ATTACHMENTS_PATTERN =
28+
"(?i)(\\.pgp$)|(\\.gpg$)|(\\.[a-zA-Z0-9]{3,4}\\.asc$)".toRegex()
29+
30+
fun decrypt(
31+
srcInputStream: InputStream,
32+
destOutputStream: OutputStream,
33+
pgpSecretKeyRingCollection: PGPSecretKeyRingCollection,
34+
protector: SecretKeyRingProtector
35+
): OpenPgpMetadata {
36+
srcInputStream.use { srcStream ->
37+
destOutputStream.use { outStream ->
38+
try {
39+
val decryptionStream = PGPainless.decryptAndOrVerify()
40+
.onInputStream(srcStream)
41+
.decryptWith(protector, pgpSecretKeyRingCollection)
42+
.doNotVerify()
43+
.build()
44+
45+
decryptionStream.use { it.copyTo(outStream) }
46+
return decryptionStream.result
47+
} catch (e: Exception) {
48+
throw processDecryptionException(e)
49+
}
50+
}
51+
}
52+
}
53+
54+
private fun processDecryptionException(e: Exception): Exception {
55+
return when (e) {
56+
is PGPException -> {
57+
when {
58+
e.message == "checksum mismatch at 0 of 20" -> {
59+
DecryptionException(DecryptionErrorType.WRONG_PASSPHRASE, e)
60+
}
61+
62+
e.message?.contains("exception decrypting session info") == true
63+
|| e.message?.contains("encoded length out of range") == true
64+
|| e.message?.contains("Exception recovering session info") == true
65+
|| e.message?.contains("No suitable decryption key") == true -> {
66+
DecryptionException(DecryptionErrorType.KEY_MISMATCH, e)
67+
}
68+
69+
else -> DecryptionException(DecryptionErrorType.OTHER, e)
70+
}
71+
}
72+
73+
is MessageNotIntegrityProtectedException -> {
74+
DecryptionException(DecryptionErrorType.NO_MDC, e)
75+
}
76+
77+
is ModificationDetectionException -> {
78+
DecryptionException(DecryptionErrorType.BAD_MDC, e)
79+
}
80+
81+
is PGPDataValidationException -> {
82+
DecryptionException(DecryptionErrorType.KEY_MISMATCH, e)
83+
}
84+
85+
is DecryptionException -> e
86+
87+
else -> DecryptionException(DecryptionErrorType.OTHER, e)
88+
}
89+
}
90+
91+
enum class DecryptionErrorType {
92+
KEY_MISMATCH,
93+
WRONG_PASSPHRASE,
94+
NO_MDC,
95+
BAD_MDC,
96+
NEED_PASSPHRASE,
97+
FORMAT,
98+
OTHER
99+
}
100+
}

FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpMsg.kt

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,8 @@ object PgpMsg {
8282

8383
private val maxTagNumber = 20
8484

85-
enum class DecryptionErrorType {
86-
KEY_MISMATCH,
87-
WRONG_PASSPHRASE,
88-
NO_MDC,
89-
BAD_MDC,
90-
NEED_PASSPHRASE,
91-
FORMAT,
92-
OTHER
93-
}
94-
9585
data class DecryptionError(
96-
val type: DecryptionErrorType,
86+
val type: PgpDecrypt.DecryptionErrorType,
9787
val message: String,
9888
val cause: Throwable? = null
9989
)
@@ -119,7 +109,7 @@ object PgpMsg {
119109
) {
120110
companion object {
121111
fun withError(
122-
type: DecryptionErrorType,
112+
type: PgpDecrypt.DecryptionErrorType,
123113
message: String,
124114
cause: Throwable? = null
125115
): DecryptionResult {
@@ -147,7 +137,10 @@ object PgpMsg {
147137
pgpPublicKeyRingCollection: PGPPublicKeyRingCollection? // for verification
148138
): DecryptionResult {
149139
if (data.isEmpty()) {
150-
return DecryptionResult.withError(DecryptionErrorType.FORMAT, "Can't decrypt empty message")
140+
return DecryptionResult.withError(
141+
type = PgpDecrypt.DecryptionErrorType.FORMAT,
142+
message = "Can't decrypt empty message"
143+
)
151144
}
152145

153146
val chunk = data.copyOfRange(0, data.size.coerceAtMost(50)).toString(StandardCharsets.US_ASCII)
@@ -162,13 +155,13 @@ object PgpMsg {
162155
ex is PGPException && ex.message != null && ex.message == "Cleartext format error"
163156
) {
164157
DecryptionResult.withError(
165-
type = DecryptionErrorType.FORMAT,
158+
type = PgpDecrypt.DecryptionErrorType.FORMAT,
166159
message = ex.message!!,
167160
cause = ex.cause
168161
)
169162
} else {
170163
DecryptionResult.withError(
171-
type = DecryptionErrorType.OTHER,
164+
type = PgpDecrypt.DecryptionErrorType.OTHER,
172165
message = "Decode cleartext error",
173166
cause = ex
174167
)
@@ -192,12 +185,12 @@ object PgpMsg {
192185
} catch (ex: PGPException) {
193186
if (ex.message == "flowcrypt: need passphrase") {
194187
return DecryptionResult.withError(
195-
type = DecryptionErrorType.NEED_PASSPHRASE,
188+
type = PgpDecrypt.DecryptionErrorType.NEED_PASSPHRASE,
196189
message = "Need passphrase"
197190
)
198191
}
199192
return DecryptionResult.withError(
200-
type = DecryptionErrorType.WRONG_PASSPHRASE,
193+
type = PgpDecrypt.DecryptionErrorType.WRONG_PASSPHRASE,
201194
message = "Wrong passphrase",
202195
cause = ex
203196
)
@@ -222,20 +215,20 @@ object PgpMsg {
222215
return DecryptionResult.withDecrypted(output, streamResult.fileInfo?.fileName)
223216
} catch (ex: MessageNotIntegrityProtectedException) {
224217
return DecryptionResult.withError(
225-
type = DecryptionErrorType.NO_MDC,
218+
type = PgpDecrypt.DecryptionErrorType.NO_MDC,
226219
message = "Security threat! Message is missing integrity checks (MDC)." +
227220
" The sender should update their outdated software.",
228221
cause = ex
229222
)
230223
} catch (ex: ModificationDetectionException) {
231224
return DecryptionResult.withError(
232-
type = DecryptionErrorType.BAD_MDC,
225+
type = PgpDecrypt.DecryptionErrorType.BAD_MDC,
233226
message = "Security threat! Integrity check failed.",
234227
cause = ex
235228
)
236229
} catch (ex: PGPDataValidationException) {
237230
return DecryptionResult.withError(
238-
type = DecryptionErrorType.KEY_MISMATCH,
231+
type = PgpDecrypt.DecryptionErrorType.KEY_MISMATCH,
239232
message = "There is no matching key",
240233
cause = ex
241234
)
@@ -247,7 +240,7 @@ object PgpMsg {
247240
|| ex.message?.contains("No suitable decryption key") == true
248241
) {
249242
return DecryptionResult.withError(
250-
type = DecryptionErrorType.KEY_MISMATCH,
243+
type = PgpDecrypt.DecryptionErrorType.KEY_MISMATCH,
251244
message = "There is no suitable decryption key",
252245
cause = ex
253246
)
@@ -258,7 +251,7 @@ object PgpMsg {
258251
exception = ex
259252
}
260253
return DecryptionResult.withError(
261-
type = DecryptionErrorType.OTHER,
254+
type = PgpDecrypt.DecryptionErrorType.OTHER,
262255
message = "Decryption failed",
263256
cause = exception
264257
)

0 commit comments

Comments
 (0)