Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ class ImportPrivateKeyActivityFromSettingsTest : BaseTest() {
.around(ScreenshotTestRule())

@Test
@ReadyForCIAnnotation
fun testImportKeyFromBackup() {
useIntentionFromRunCheckKeysActivity()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.api.email.gmail
Expand All @@ -16,7 +18,6 @@ import com.flowcrypt.email.api.email.gmail.api.GMailRawAttachmentFilterInputStre
import com.flowcrypt.email.api.email.gmail.api.GMailRawMIMEMessageFilterInputStream
import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.api.email.model.LocalFolder
import com.flowcrypt.email.api.retrofit.node.NodeCallsExecutor
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails
import com.flowcrypt.email.database.FlowCryptRoomDatabase
Expand All @@ -27,6 +28,7 @@ import com.flowcrypt.email.extensions.contentId
import com.flowcrypt.email.extensions.disposition
import com.flowcrypt.email.extensions.isMimeType
import com.flowcrypt.email.extensions.uid
import com.flowcrypt.email.security.pgp.PgpKey
import com.flowcrypt.email.ui.notifications.ErrorNotificationManager
import com.flowcrypt.email.util.exception.CommonConnectionException
import com.flowcrypt.email.util.exception.ExceptionUtil
Expand Down Expand Up @@ -668,7 +670,7 @@ class GmailApiHelper {
}

try {
list.addAll(NodeCallsExecutor.parseKeys(backup))
list.addAll(PgpKey.parseKeysC(backup.toByteArray()))
} catch (e: NodeException) {
e.printStackTrace()
ExceptionUtil.handleError(e)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.api.retrofit.response.model.node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ import com.flowcrypt.email.api.retrofit.response.model.node.Algo
import com.flowcrypt.email.api.retrofit.response.model.node.KeyId
import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails
import com.flowcrypt.email.security.pgp.PgpArmor
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.util.concurrent.TimeUnit
import org.bouncycastle.bcpg.ArmoredOutputStream
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing
Expand All @@ -24,6 +19,11 @@ import org.pgpainless.key.OpenPgpV4Fingerprint
import org.pgpainless.key.generation.type.eddsa.EdDSACurve
import org.pgpainless.key.info.KeyInfo
import org.pgpainless.key.info.KeyRingInfo
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.time.Instant
import java.util.concurrent.TimeUnit

/**
* @author Denis Bondarenko
Expand Down Expand Up @@ -56,11 +56,11 @@ fun PGPKeyRing.toNodeKeyDetails(): NodeKeyDetails {
)
}

val privateKey = if (keyRingInfo.isSecretKey) this.armor(PgpArmor.FLOWCRYPT_HEADERS) else null
val privateKey = if (keyRingInfo.isSecretKey) armor() else null
val publicKey = if (keyRingInfo.isSecretKey) {
(this as PGPSecretKeyRing).toPublicKeyRing().armor(PgpArmor.FLOWCRYPT_HEADERS)
(this as PGPSecretKeyRing).toPublicKeyRing().armor()
} else {
this.armor(PgpArmor.FLOWCRYPT_HEADERS)
armor()
}

return NodeKeyDetails(
Expand All @@ -80,7 +80,7 @@ fun PGPKeyRing.toNodeKeyDetails(): NodeKeyDetails {
}

@Throws(IOException::class)
fun PGPKeyRing.armor(headers: List<Pair<String, String>>? = null): String {
fun PGPKeyRing.armor(headers: List<Pair<String, String>>? = PgpArmor.FLOWCRYPT_HEADERS): String {
ByteArrayOutputStream().use { out ->
ArmoredOutputStream(out).use { armoredOut ->
if (headers != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.jetpack.viewmodel
Expand All @@ -15,10 +17,10 @@ import com.flowcrypt.email.api.email.EmailUtil
import com.flowcrypt.email.api.email.SearchBackupsUtil
import com.flowcrypt.email.api.email.gmail.GmailApiHelper
import com.flowcrypt.email.api.email.protocol.OpenStoreHelper
import com.flowcrypt.email.api.retrofit.node.NodeCallsExecutor
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.security.pgp.PgpKey
import com.flowcrypt.email.util.exception.ExceptionUtil
import com.flowcrypt.email.util.exception.NodeException
import com.google.android.gms.auth.GoogleAuthException
Expand Down Expand Up @@ -111,7 +113,7 @@ class LoadPrivateKeysViewModel(application: Application) : BaseAndroidViewModel(
}

try {
details.addAll(NodeCallsExecutor.parseKeys(backup))
details.addAll(PgpKey.parseKeysC(backup.toByteArray()))
} catch (e: NodeException) {
e.printStackTrace()
ExceptionUtil.handleError(e)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.jetpack.viewmodel
Expand All @@ -22,7 +24,6 @@ import com.flowcrypt.email.api.email.protocol.OpenStoreHelper
import com.flowcrypt.email.api.email.protocol.SmtpProtocolUtil
import com.flowcrypt.email.api.retrofit.ApiRepository
import com.flowcrypt.email.api.retrofit.FlowcryptApiRepository
import com.flowcrypt.email.api.retrofit.node.NodeCallsExecutor
import com.flowcrypt.email.api.retrofit.node.NodeRepository
import com.flowcrypt.email.api.retrofit.node.PgpApiRepository
import com.flowcrypt.email.api.retrofit.request.model.InitialLegacySubmitModel
Expand All @@ -40,6 +41,7 @@ import com.flowcrypt.email.model.PgpContact
import com.flowcrypt.email.security.KeyStoreCryptoManager
import com.flowcrypt.email.security.KeysStorageImpl
import com.flowcrypt.email.security.SecurityUtils
import com.flowcrypt.email.security.pgp.PgpKey
import com.flowcrypt.email.service.actionqueue.actions.BackupPrivateKeyToInboxAction
import com.flowcrypt.email.service.actionqueue.actions.RegisterUserPublicKeyAction
import com.flowcrypt.email.service.actionqueue.actions.SendWelcomeTestEmailAction
Expand Down Expand Up @@ -375,7 +377,7 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl
newPassphrase: String,
originalPrivateKey: String?): NodeKeyDetails =
withContext(Dispatchers.IO) {
val keyDetailsList = NodeCallsExecutor.parseKeys(originalPrivateKey!!)
val keyDetailsList = PgpKey.parseKeysC(originalPrivateKey!!.toByteArray())
if (CollectionUtils.isEmpty(keyDetailsList) || keyDetailsList.size != 1) {
throw IllegalStateException("Parse keys error")
}
Expand All @@ -387,19 +389,19 @@ class PrivateKeysViewModel(application: Application) : BaseNodeApiViewModel(appl
throw IllegalStateException("Passphrase for key with longid $longId not found")
}

val (decryptedKey) = NodeCallsExecutor.decryptKey(nodeKeyDetails.privateKey!!, oldPassphrase!!)
val decryptedKey = PgpKey.decryptKey(nodeKeyDetails.privateKey!!, oldPassphrase!!)

if (TextUtils.isEmpty(decryptedKey)) {
throw IllegalStateException("Can't decrypt key with longid " + longId!!)
}

val (encryptedKey) = NodeCallsExecutor.encryptKey(decryptedKey!!, newPassphrase)
val encryptedKey = PgpKey.encryptKey(decryptedKey, newPassphrase)

if (TextUtils.isEmpty(encryptedKey)) {
throw IllegalStateException("Can't encrypt key with longid " + longId!!)
}

val modifiedKeyDetailsList = NodeCallsExecutor.parseKeys(encryptedKey!!)
val modifiedKeyDetailsList = PgpKey.parseKeysC(encryptedKey.toByteArray())
if (CollectionUtils.isEmpty(modifiedKeyDetailsList) || modifiedKeyDetailsList.size != 1) {
throw IllegalStateException("Parse keys error")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.security
Expand All @@ -12,6 +14,7 @@ import com.flowcrypt.email.R
import com.flowcrypt.email.api.retrofit.node.NodeCallsExecutor
import com.flowcrypt.email.database.FlowCryptRoomDatabase
import com.flowcrypt.email.database.entity.AccountEntity
import com.flowcrypt.email.security.pgp.PgpKey
import com.flowcrypt.email.util.exception.DifferentPassPhrasesException
import com.flowcrypt.email.util.exception.NoKeyAvailableException
import com.flowcrypt.email.util.exception.NoPrivateKeysAvailableException
Expand Down Expand Up @@ -93,12 +96,11 @@ class SecurityUtils {
else -> throw IllegalArgumentException(context.getString(R.string.missing_pass_phrase_strength_evaluation))
}

val nodeKeyDetailsList = NodeCallsExecutor.parseKeys(private)
val nodeKeyDetailsList = PgpKey.parseKeysC(private.toByteArray())
val keyDetails = nodeKeyDetailsList.first()

val encryptedKey = if (keyDetails.isFullyDecrypted == true) {
val encryptKeResult = NodeCallsExecutor.encryptKey(private, passPhrase)
encryptKeResult.encryptedKey
PgpKey.encryptKey(private, passPhrase)
} else {
keyDetails.privateKey
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ package com.flowcrypt.email.security.pgp
import com.flowcrypt.email.api.retrofit.response.model.node.MsgBlock
import com.flowcrypt.email.api.retrofit.response.model.node.NodeKeyDetails
import com.flowcrypt.email.core.msg.MsgBlockParser
import com.flowcrypt.email.extensions.pgp.armor
import com.flowcrypt.email.extensions.pgp.toNodeKeyDetails
import java.nio.charset.StandardCharsets
import org.bouncycastle.bcpg.ArmoredInputStream
import org.bouncycastle.openpgp.PGPKeyRing
import org.bouncycastle.openpgp.PGPObjectFactory
Expand All @@ -21,29 +21,59 @@ import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRingCollection
import org.bouncycastle.openpgp.jcajce.JcaPGPSecretKeyRingCollection
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator
import org.pgpainless.PGPainless
import org.pgpainless.util.Passphrase
import java.nio.charset.StandardCharsets

object PgpKey {
fun encryptKey(armored: String, passphrase: String): String {
return try {
val keys = parseAndNormalizeKeyRings(armored)
PGPainless.modifyKeyRing(keys[0] as PGPSecretKeyRing)
.changePassphraseFromOldPassphrase(null)
.withSecureDefaultSettings()
.toNewPassphrase(Passphrase.fromPassword(passphrase))
.done()
.armor()
} catch (e: Exception) {
""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IvanPizhenko Please tell me is there any reason to don't print the stacktrace of an exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DenBond7 This was done to mimic what the Typescript does - it returns empty string on failure. Of course, we can output the exception. But I am just not sure what is the right way to do it on the Android? Just println()? Some logger? Please advice.

Copy link
Contributor Author

@IvanPizhenko IvanPizhenko Mar 23, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked around - looks like just e.printStackTrace() would work good - will add

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, e.printStackTrace() - it will be enough. Anyway, don't need to drop using e.printStackTrace() over the whole Java(Kotlin) code if you don't have enough reason to do that. Usually, it helps to debug errors.

}
}

fun decryptKey(armored: String, passphrase: String): String {
return try {
val keys = parseAndNormalizeKeyRings(armored)
PGPainless.modifyKeyRing(keys[0] as PGPSecretKeyRing)
.changePassphraseFromOldPassphrase(Passphrase.fromPassword(passphrase))
.withSecureDefaultSettings()
.toNoPassphrase()
.done()
.armor()
} catch (ex: Exception) {
""
}
}

/**
* Parses multiple keys, binary or armored.
*
* @return Pair.first indicates armored (true) or binary (false) format
* Pair.second list of keys
*/
fun parseKeys(source: ByteArray): Pair<Boolean, List<NodeKeyDetails>> {
fun parseKeys(source: ByteArray): Pair<Boolean, List<PGPKeyRing>> {
val blockType = PgpMsg.detectBlockType(source)
if (blockType.second == MsgBlock.Type.UNKNOWN) {
throw IllegalArgumentException("Unknown message type")
}

val allKeys = mutableListOf<NodeKeyDetails>()
val allKeys = mutableListOf<PGPKeyRing>()
if (blockType.first) {
// armored text format
val blocks = MsgBlockParser.detectBlocks(String(source, StandardCharsets.UTF_8))
for (block in blocks) {
val content = block.content
if (content != null) {
val keys = parse(content)
val keys = parseAndNormalizeKeyRings(content)
allKeys.addAll(keys)
}
}
Expand All @@ -53,24 +83,33 @@ object PgpKey {
while (true) {
val obj = objectFactory.nextObject() ?: break
if (obj is PGPKeyRing) {
allKeys.add(obj.toNodeKeyDetails())
allKeys.add(obj)
}
}
}

return Pair(blockType.first, allKeys)
}

fun parse(armored: String): List<NodeKeyDetails>
= parseAndNormalizeKeyRings(armored).map { it.toNodeKeyDetails() }.toList()
/**
* Parse a list of [NodeKeyDetails] from the given string. It can take one key or many keys, it can be
* private or public keys, it can be armored or binary... doesn't matter.
*
* This method should be dropped in the future. Currently it should be used just for compatibility.
*
* @return list of keys
*/
fun parseKeysC(source: ByteArray): List<NodeKeyDetails> {
return parseKeys(source).second.map { it.toNodeKeyDetails() }
}

private fun parseAndNormalizeKeyRings(armored: String): List<PGPKeyRing> {
val normalizedArmored = PgpArmor.normalize(armored, MsgBlock.Type.UNKNOWN)
val keys = mutableListOf<PGPKeyRing>()
if (PgpArmor.ARMOR_HEADER_DICT_REGEX[MsgBlock.Type.PUBLIC_KEY]!!
.beginRegexp.containsMatchIn(normalizedArmored)) {
val keyRingCollection = JcaPGPPublicKeyRingCollection(
ArmoredInputStream(normalizedArmored.toByteArray(StandardCharsets.UTF_8).inputStream())
ArmoredInputStream(normalizedArmored.toByteArray(StandardCharsets.UTF_8).inputStream())
)
// We have to use reflection because BouncyCastle declares "order" list as a private field
// https://stackoverflow.com/a/1196207/1540501
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors:
* DenBond7
* Ivan Pizhenko
*/

package com.flowcrypt.email.service
Expand All @@ -18,9 +20,9 @@ import android.os.Message
import android.os.Messenger
import android.os.RemoteException
import android.text.TextUtils
import com.flowcrypt.email.api.retrofit.node.NodeCallsExecutor
import com.flowcrypt.email.model.KeyDetails
import com.flowcrypt.email.model.KeyImportModel
import com.flowcrypt.email.security.pgp.PgpKey
import com.flowcrypt.email.util.LogsUtil
import com.flowcrypt.email.util.exception.ExceptionUtil
import com.google.android.gms.common.util.CollectionUtils
Expand Down Expand Up @@ -111,7 +113,7 @@ class CheckClipboardToFindKeyService : Service(), ClipboardManager.OnPrimaryClip
* The incoming handler realization. This handler will be used to communicate with current
* service and the worker thread.
*/
private class ReplyHandler internal constructor(checkClipboardToFindKeyService: CheckClipboardToFindKeyService)
private class ReplyHandler(checkClipboardToFindKeyService: CheckClipboardToFindKeyService)
: Handler() {
private val weakRef: WeakReference<CheckClipboardToFindKeyService> = WeakReference(checkClipboardToFindKeyService)

Expand All @@ -138,14 +140,14 @@ class CheckClipboardToFindKeyService : Service(), ClipboardManager.OnPrimaryClip
* This handler will be used by the instance of [HandlerThread] to receive message from
* the UI thread.
*/
private class ServiceWorkerHandler internal constructor(looper: Looper) : Handler(looper) {
private class ServiceWorkerHandler(looper: Looper) : Handler(looper) {

override fun handleMessage(msg: Message) {
when (msg.what) {
MESSAGE_WHAT -> {
val clipboardText = msg.obj as String
try {
val nodeKeyDetails = NodeCallsExecutor.parseKeys(clipboardText)
val nodeKeyDetails = PgpKey.parseKeysC(clipboardText.toByteArray())
if (!CollectionUtils.isEmpty(nodeKeyDetails)) {
sendReply(msg)
}
Expand Down
Loading