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
2 changes: 2 additions & 0 deletions FlowCrypt/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,8 @@ dependencies {
implementation 'net.openid:appauth:0.9.1'
implementation 'org.bitbucket.b_c:jose4j:0.7.8'
implementation 'me.everything:overscroll-decor-android:1.1.0'
implementation 'com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1'
implementation 'org.jsoup:jsoup:1.13.1'

implementation ('org.pgpainless:pgpainless-core:0.2.3') {
//exclude group: 'org.bouncycastle' because we will specify it manually
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
/*
* © 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

import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.Expose
import org.json.JSONObject

/**
* @author Denis Bondarenko
Expand All @@ -19,14 +22,27 @@ data class AttMeta(
@Expose val name: String?,
@Expose var data: String?,
@Expose val length: Long,
@Expose val type: String?
@Expose val type: String?,
@Expose val contentId: String?,
@Expose val url: String? = null
) : Parcelable {

constructor(source: JSONObject) : this(
name = if (source.has("name")) source.getString("name") else null,
data = if (source.has("data")) source.getString("data") else null,
length = if (source.has("size")) source.getLong("size") else 0L,
type = if (source.has("type")) source.getString("type") else null,
contentId = if (source.has("contentId")) source.getString("contentId") else null,
url = if (source.has("url")) source.getString("url") else null
)

constructor(source: Parcel) : this(
source.readString(),
source.readString(),
source.readLong(),
source.readString()
name = source.readString(),
data =source.readString(),
length = source.readLong(),
type = source.readString(),
contentId = source.readString(),
url = source.readString()
)

override fun describeContents() = 0
Expand All @@ -36,6 +52,8 @@ data class AttMeta(
writeString(data)
writeLong(length)
writeString(type)
writeString(contentId)
writeString(url)
}

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

package com.flowcrypt.email.api.retrofit.response.model.node

interface AttMsgBlock : MsgBlock {
val attMeta: AttMeta
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package com.flowcrypt.email.api.retrofit.response.model.node

import android.os.Parcel
import android.os.Parcelable
import com.flowcrypt.email.security.pgp.PgpMsg
import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

Expand All @@ -19,7 +20,9 @@ import com.google.gson.annotations.SerializedName
data class DecryptErrorMsgBlock(
@Expose override val content: String?,
@Expose override val complete: Boolean,
@SerializedName("decryptErr") @Expose val error: DecryptError?
@SerializedName("decryptErr") @Expose val error: DecryptError?,
// TODO: remove above and change name of below when finally dropping Node
@SerializedName("kotlinDecryptErr") @Expose val kotlinError: PgpMsg.DecryptionError? = null
) : MsgBlock {

@Expose
Expand All @@ -28,7 +31,8 @@ data class DecryptErrorMsgBlock(
constructor(source: Parcel) : this(
source.readString(),
1 == source.readInt(),
source.readParcelable<DecryptError>(DecryptError::class.java.classLoader)
source.readParcelable<DecryptError>(DecryptError::class.java.classLoader),
source.readParcelable<PgpMsg.DecryptionError>(PgpMsg.DecryptionError::class.java.classLoader)
)

override fun describeContents(): Int {
Expand All @@ -39,8 +43,9 @@ data class DecryptErrorMsgBlock(
with(dest) {
writeParcelable(type, flags)
writeString(content)
writeInt((if (complete) 1 else 0))
writeInt(if (complete) 1 else 0)
writeParcelable(error, flags)
writeParcelable(kotlinError, flags)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import com.google.gson.annotations.SerializedName
data class DecryptedAttMsgBlock(
@Expose override val content: String?,
@Expose override val complete: Boolean,
@Expose val attMeta: AttMeta,
@Expose override val attMeta: AttMeta,
@SerializedName("decryptErr") @Expose val error: DecryptError?
) : MsgBlock {
) : AttMsgBlock {

var fileUri: Uri? = null

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

package com.flowcrypt.email.api.retrofit.response.model.node

import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.Expose

data class EncryptedAttLinkMsgBlock(
@Expose override val attMeta: AttMeta,
) : AttMsgBlock {

@Expose
override val content: String = ""
@Expose
override val type: MsgBlock.Type = MsgBlock.Type.ENCRYPTED_ATT_LINK
@Expose
override val complete: Boolean = true

constructor(source: Parcel) : this(
source.readParcelable<AttMeta>(AttMeta::class.java.classLoader)!!
)

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeParcelable(attMeta, flags)
}

override fun describeContents(): Int {
return 0
}

companion object CREATOR : Parcelable.Creator<EncryptedAttLinkMsgBlock> {
override fun createFromParcel(parcel: Parcel): EncryptedAttLinkMsgBlock {
return EncryptedAttLinkMsgBlock(parcel)
}

override fun newArray(size: Int): Array<EncryptedAttLinkMsgBlock?> {
return arrayOfNulls(size)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import android.os.Parcel
import android.os.Parcelable
import com.google.gson.annotations.Expose

data class EncryptedAttMsgBlock(@Expose override val content: String?,
@Expose val attMeta: AttMeta) : MsgBlock {
data class EncryptedAttMsgBlock(
@Expose override val content: String?,
@Expose override val attMeta: AttMeta
) : AttMsgBlock {

var fileUri: Uri? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,10 @@ interface MsgBlock : Parcelable {
SIGNED_TEXT,

@SerializedName("signedHtml")
SIGNED_HTML;
SIGNED_HTML,

@SerializedName("verifiedMsg")
VERIFIED_MSG;

override fun describeContents(): Int {
return 0
Expand All @@ -86,24 +89,30 @@ interface MsgBlock : Parcelable {
dest.writeInt(ordinal)
}

fun isContentBlockType() : Boolean = CONTENT_BLOCK_TYPES.contains(this)

companion object {
@JvmField
val CREATOR: Parcelable.Creator<Type> = object : Parcelable.Creator<Type> {
override fun createFromParcel(source: Parcel): Type = values()[source.readInt()]
override fun newArray(size: Int): Array<Type?> = arrayOfNulls(size)
}

val keyBlockTypes = setOf(PUBLIC_KEY, PRIVATE_KEY)
val KEY_BLOCK_TYPES = setOf(PUBLIC_KEY, PRIVATE_KEY)

val replaceableBlockTypes = setOf(
val REPLACEABLE_BLOCK_TYPES = setOf(
PUBLIC_KEY, PRIVATE_KEY, SIGNED_MSG, ENCRYPTED_MSG, ENCRYPTED_MSG_LINK
)

val wellKnownBlockTypes = setOf(
val WELL_KNOWN_BLOCK_TYPES = setOf(
PUBLIC_KEY, PRIVATE_KEY, SIGNED_MSG, ENCRYPTED_MSG
)

val signedBlocks = setOf(SIGNED_TEXT, SIGNED_HTML, SIGNED_MSG)
val SIGNED_BLOCK_TYPES = setOf(SIGNED_TEXT, SIGNED_HTML, SIGNED_MSG)

val CONTENT_BLOCK_TYPES = setOf(
PLAIN_TEXT, PLAIN_HTML, DECRYPTED_TEXT, DECRYPTED_HTML, SIGNED_MSG, VERIFIED_MSG
)

fun ofSerializedName(serializedName: String): Type {
for (v in values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
package com.flowcrypt.email.api.retrofit.response.model.node

import android.os.Parcel
import com.flowcrypt.email.extensions.java.io.toBase64EncodedString
import com.flowcrypt.email.security.pgp.PgpKey
import java.io.InputStream
import java.util.Base64
import javax.mail.internet.MimePart

object MsgBlockFactory {
@JvmStatic
val supportedMsgBlockTypes = listOf(
MsgBlock.Type.PUBLIC_KEY,
MsgBlock.Type.DECRYPT_ERROR,
Expand All @@ -18,21 +22,19 @@ object MsgBlockFactory {
MsgBlock.Type.SIGNED_TEXT
)

@JvmStatic
fun fromParcel(type: MsgBlock.Type, source: Parcel): MsgBlock {
return when (type) {
MsgBlock.Type.PUBLIC_KEY -> PublicKeyMsgBlock(source)
MsgBlock.Type.DECRYPT_ERROR -> DecryptErrorMsgBlock(source)
MsgBlock.Type.DECRYPTED_ATT -> DecryptedAttMsgBlock(source)
MsgBlock.Type.ENCRYPTED_ATT -> EncryptedAttMsgBlock(source)
MsgBlock.Type.SIGNED_TEXT, MsgBlock.Type.SIGNED_HTML, MsgBlock.Type.SIGNED_MSG -> {
SignedBlock(source)
SignedMsgBlock(source)
}
else -> GenericMsgBlock(type, source)
}
}

@JvmStatic
fun fromContent(
type: MsgBlock.Type,
content: String?,
Expand All @@ -41,20 +43,38 @@ object MsgBlockFactory {
): MsgBlock {
val complete = !missingEnd
return when (type) {
MsgBlock.Type.PUBLIC_KEY -> PublicKeyMsgBlock(content, complete, null)
MsgBlock.Type.PUBLIC_KEY -> {
val keyDetails = if (content != null && complete) {
PgpKey.parseKeys(content).toPgpKeyDetailsList().firstOrNull()
} else null
PublicKeyMsgBlock(content, complete, keyDetails)
}
MsgBlock.Type.DECRYPT_ERROR -> DecryptErrorMsgBlock(content, complete, null)
MsgBlock.Type.SIGNED_TEXT -> {
SignedBlock(SignedBlock.Type.SIGNED_TEXT, content, complete, signature)
SignedMsgBlock(SignedMsgBlock.Type.SIGNED_TEXT, content, complete, signature)
}
MsgBlock.Type.SIGNED_HTML -> {
SignedBlock(SignedBlock.Type.SIGNED_HTML, content, complete, signature)
SignedMsgBlock(SignedMsgBlock.Type.SIGNED_HTML, content, complete, signature)
}
else -> GenericMsgBlock(type, content, complete)
}
}

@JvmStatic
fun fromAttachment(type: MsgBlock.Type, content: String?, attMeta: AttMeta): MsgBlock {
fun fromAttachment(type: MsgBlock.Type, attachment: MimePart): MsgBlock {
val attContent = attachment.content
val data: String? = when (attContent) {
is String -> Base64.getEncoder().encodeToString(attachment.inputStream.readBytes())
is InputStream -> attContent.toBase64EncodedString()
else -> null
}
val attMeta = AttMeta(
name = attachment.fileName,
data = data,
length = attachment.size.toLong(),
type = attachment.contentType,
contentId = attachment.contentID
)
val content = if (attContent is String) attachment.content as String else null
return when (type) {
MsgBlock.Type.DECRYPTED_ATT -> DecryptedAttMsgBlock(content, true, attMeta, null)
MsgBlock.Type.ENCRYPTED_ATT -> EncryptedAttMsgBlock(content, attMeta)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import com.google.gson.annotations.Expose

data class PlainAttMsgBlock(
@Expose override val content: String?,
@Expose val attMeta: AttMeta
) : MsgBlock {
@Expose override val attMeta: AttMeta
) : AttMsgBlock {

var fileUri: Uri? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import com.google.gson.annotations.Expose
/**
* Message block which represents content with a signature.
*/
data class SignedBlock(
data class SignedMsgBlock(
@Expose val signedType: Type,
@Expose override val content: String?,
@Expose override val complete: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors:
* Ivan Pizhenko
*/

package com.flowcrypt.email.core.msg

import com.flowcrypt.email.api.retrofit.response.model.node.MsgBlock
import com.flowcrypt.email.api.retrofit.response.model.node.PlainAttMsgBlock
import com.flowcrypt.email.extensions.kotlin.toInputStream
import java.nio.charset.StandardCharsets
import java.util.Locale
import java.util.Properties
import javax.mail.Session
import javax.mail.internet.MimeMessage

object MimeUtils {
fun resemblesMsg(msg: ByteArray?): Boolean {
if (msg == null) return false
val firstChars = msg.copyOfRange(0, msg.size.coerceAtMost(1000))
.toString(StandardCharsets.US_ASCII)
.toLowerCase(Locale.ROOT)
Comment on lines +21 to +23
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wonder if the conversion to String can fail with an exception if we give it arbitrary bytes that are non-ascii. If it can fail by throwing, we should catch the error and return false.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did the small test:

import java.nio.charset.StandardCharsets
import java.util.Locale

fun main() {
    val a = ByteArray(256)
    for (i in 0..255) a[i] = i.toByte()
    val s = a.toString(StandardCharsets.US_ASCII).toLowerCase(Locale.ROOT)
    println(s)
}

Works for all source byte values.

val contentType = CONTENT_TYPE_REGEX.find(firstChars) ?: return false
return CONTENT_TRANSFER_ENCODING_REGEX.containsMatchIn(firstChars)
|| CONTENT_DISPOSITION_REGEX.containsMatchIn(firstChars)
|| firstChars.contains(BOUNDARY_1)
|| firstChars.contains(CHARSET)
|| (contentType.range.first == 0 && firstChars.contains(BOUNDARY_2))
}

fun isPlainImgAtt(block: MsgBlock): Boolean {
return (block is PlainAttMsgBlock)
&& block.attMeta.type != null
&& imageContentTypes.contains(block.attMeta.type)
}

fun mimeTextToMimeMessage(mimeText: String) : MimeMessage {
return MimeMessage(Session.getInstance(Properties()), mimeText.toInputStream())
}

private val imageContentTypes = setOf(
"image/jpeg", "image/jpg", "image/bmp", "image/png", "image/svg+xml"
)

private val CONTENT_TYPE_REGEX = Regex("content-type: +[0-9a-z\\-/]+")
private val CONTENT_TRANSFER_ENCODING_REGEX = Regex("content-transfer-encoding: +[0-9a-z\\-/]+")
private val CONTENT_DISPOSITION_REGEX = Regex("content-disposition: +[0-9a-z\\-/]+")
private const val BOUNDARY_1 = "; boundary="
private const val BOUNDARY_2 = "boundary="
private const val CHARSET = "; charset="
}
Loading