Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors: denbond7
*/

package com.flowcrypt.email.api.email.javamail
Expand All @@ -10,6 +10,7 @@ import android.net.Uri
import android.text.TextUtils
import com.flowcrypt.email.Constants
import com.flowcrypt.email.api.email.model.AttachmentInfo
import com.flowcrypt.email.util.OutgoingAttachmentUriValidator
import jakarta.activation.DataSource
import java.io.BufferedInputStream
import java.io.InputStream
Expand All @@ -25,6 +26,7 @@ open class AttachmentInfoDataSource(private val context: Context, val att: Attac

override fun getInputStream(): InputStream? {
return att.uri?.let { uri ->
OutgoingAttachmentUriValidator.requireAllowedUri(context, uri)
context.contentResolver.openInputStream(uri)?.let { stream -> BufferedInputStream(stream) }
} ?: att.rawData?.inputStream()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.flowcrypt.email.model.MessageEncryptionType
import com.flowcrypt.email.security.SecurityUtils
import com.flowcrypt.email.security.pgp.PgpEncryptAndOrSign
import com.flowcrypt.email.util.FileAndDirectoryUtils
import com.flowcrypt.email.util.OutgoingAttachmentUriValidator
import jakarta.mail.Message
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -119,6 +120,7 @@ object ProcessingOutgoingMessageInfoHelper {
val origFileUri = attachmentInfo.uri
var originalFileInputStream: InputStream? = null
if (origFileUri != null) {
OutgoingAttachmentUriValidator.requireAllowedUri(context, origFileUri)
originalFileInputStream = context.contentResolver.openInputStream(origFileUri)
} else if (attachmentInfo.rawData?.isNotEmpty() == true) {
originalFileInputStream = ByteArrayInputStream(attachmentInfo.rawData)
Expand Down Expand Up @@ -173,6 +175,7 @@ object ProcessingOutgoingMessageInfoHelper {
}

for (candidate in outgoingMsgInfo.forwardedAtts ?: emptyList()) {
candidate.uri?.let { OutgoingAttachmentUriValidator.requireAllowedUri(context, it) }
if (candidate.isEncryptionAllowed
&& outgoingMsgInfo.encryptionType === MessageEncryptionType.ENCRYPTED
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.flowcrypt.email.extensions.incrementSafely
import com.flowcrypt.email.extensions.toast
import com.flowcrypt.email.model.MessageEncryptionType
import com.flowcrypt.email.model.MessageType
import com.flowcrypt.email.ui.activity.fragment.CreateMessageFragmentArgs
import com.flowcrypt.email.ui.activity.fragment.dialog.ChoosePublicKeyDialogFragment
import com.flowcrypt.email.util.FileAndDirectoryUtils
import com.flowcrypt.email.util.FlavorSettings
Expand Down Expand Up @@ -73,16 +74,39 @@ class CreateMessageActivity : BaseActivity<ActivityCreateMessageBinding>(),
}

override fun onCreate(savedInstanceState: Bundle?) {
sanitizeIntentForNavigation(intent)
enableEdgeToEdge()
super.onCreate(savedInstanceState)
(navController as? NavHostController)?.enableOnBackPressed(true)
isNavigationArrowDisplayed = true
val navGraph = navController.navInflater.inflate(R.navigation.create_msg_graph)
navController.setGraph(navGraph, intent.extras)
navController.setGraph(navGraph, createStartDestinationArgs(intent))
FileAndDirectoryUtils.cleanDir(File(cacheDir, Constants.DRAFT_CACHE_DIR))
applyInsetsToSupportEdgeToEdge()
}

override fun onNewIntent(intent: Intent) {
sanitizeIntentForNavigation(intent)
setIntent(intent)
super.onNewIntent(intent)
}

private fun sanitizeIntentForNavigation(intent: Intent) {
val originalExtras = intent.extras ?: return
val sanitizedExtras = Bundle(originalExtras).apply {
NAVIGATION_DEEP_LINK_EXTRA_KEYS.forEach(::remove)
}
intent.replaceExtras(sanitizedExtras)
}

private fun createStartDestinationArgs(intent: Intent): Bundle? {
return if (intent.action in PUBLIC_INTENT_ACTIONS) {
Bundle.EMPTY
} else {
intent.extras?.let { CreateMessageFragmentArgs.fromBundle(it).toBundle() }
}
}

override fun onAccountInfoRefreshed(accountEntity: AccountEntity?) {
super.onAccountInfoRefreshed(accountEntity)
//check create a message from extra info when account didn't setup
Expand Down Expand Up @@ -111,6 +135,25 @@ class CreateMessageActivity : BaseActivity<ActivityCreateMessageBinding>(),
}

companion object {
private const val EXTRA_KEY_INCOMING_MESSAGE_INFO = "incomingMessageInfo"
private const val EXTRA_KEY_ATTACHMENTS = "attachments"
private const val EXTRA_KEY_MESSAGE_TYPE = "messageType"
private const val EXTRA_KEY_ENCRYPTED_BY_DEFAULT = "encryptedByDefault"
private const val EXTRA_KEY_SERVICE_INFO = "serviceInfo"
private val NAVIGATION_DEEP_LINK_EXTRA_KEYS = setOf(
"android-support-nav:controller:deepLinkIds",
"android-support-nav:controller:deepLinkArgs",
"android-support-nav:controller:deepLinkExtras",
"android-support-nav:controller:deepLinkHandled",
"android-support-nav:controller:deepLinkIntent",
)
private val PUBLIC_INTENT_ACTIONS = setOf(
Intent.ACTION_VIEW,
Intent.ACTION_SENDTO,
Intent.ACTION_SEND,
Intent.ACTION_SEND_MULTIPLE
)

fun generateIntent(
context: Context?,
@MessageType messageType: Int,
Expand All @@ -121,11 +164,14 @@ class CreateMessageActivity : BaseActivity<ActivityCreateMessageBinding>(),
): Intent {
val intent = Intent(context, CreateMessageActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra("incomingMessageInfo", msgInfo)
intent.putExtra("attachments", attachments)
intent.putExtra("messageType", messageType)
intent.putExtra("encryptedByDefault", msgEncryptionType == MessageEncryptionType.ENCRYPTED)
intent.putExtra("serviceInfo", serviceInfo)
intent.putExtra(EXTRA_KEY_INCOMING_MESSAGE_INFO, msgInfo)
intent.putExtra(EXTRA_KEY_ATTACHMENTS, attachments)
intent.putExtra(EXTRA_KEY_MESSAGE_TYPE, messageType)
intent.putExtra(
EXTRA_KEY_ENCRYPTED_BY_DEFAULT,
msgEncryptionType == MessageEncryptionType.ENCRYPTED
)
intent.putExtra(EXTRA_KEY_SERVICE_INFO, serviceInfo)
return intent
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors: denbond7
*/

package com.flowcrypt.email.ui.activity.fragment.dialog
Expand All @@ -17,8 +17,9 @@ import androidx.lifecycle.ViewModel
import androidx.navigation.fragment.navArgs
import com.flowcrypt.email.api.retrofit.response.base.Result
import com.flowcrypt.email.databinding.FragmentCreateOutgoingMessageBinding
import com.flowcrypt.email.extensions.launchAndRepeatWithLifecycle
import com.flowcrypt.email.extensions.androidx.fragment.app.navController
import com.flowcrypt.email.extensions.androidx.fragment.app.toast
import com.flowcrypt.email.extensions.launchAndRepeatWithLifecycle
import com.flowcrypt.email.extensions.visible
import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory
import com.flowcrypt.email.jetpack.viewmodel.CreateOutgoingMessageViewModel
Expand All @@ -43,6 +44,8 @@ class CreateOutgoingMessageDialogFragment : BaseDialogFragment() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toast("SENDING!!!!")

isCancelable = false
collectCreateOutgoingMessageStateFlow()
createOutgoingMessageViewModel.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: denbond7
*/

package com.flowcrypt.email.util

import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.flowcrypt.email.Constants
import com.flowcrypt.email.providers.EmbeddedAttachmentsProvider
import java.io.File
import java.io.IOException
import java.util.Locale

/**
* Validates attachment URIs used by outgoing messages.
*
* Outgoing flows may only use content provided by FlowCrypt itself or files staged inside
* FlowCrypt-controlled cache directories for the current compose/send session.
*/
object OutgoingAttachmentUriValidator {
private val allowedContentAuthorities = setOf(
Constants.FILE_PROVIDER_AUTHORITY.lowercase(Locale.ROOT),
EmbeddedAttachmentsProvider.Cache.AUTHORITY.lowercase(Locale.ROOT)
)

@Throws(IllegalArgumentException::class, IOException::class)
fun requireAllowedUri(context: Context, uri: Uri) {
when (uri.scheme?.lowercase(Locale.ROOT)) {
ContentResolver.SCHEME_CONTENT -> requireAllowedContentUri(uri)
ContentResolver.SCHEME_FILE -> requireAllowedFileUri(context, uri)
else -> throw IllegalArgumentException("Unsupported attachment URI scheme: ${uri.scheme}")
}
}

private fun requireAllowedContentUri(uri: Uri) {
val authority = uri.authority?.lowercase(Locale.ROOT)
?: throw IllegalArgumentException("Attachment content URI has no authority")

if (authority !in allowedContentAuthorities) {
throw IllegalArgumentException("Attachment content URI authority is not allowed: $authority")
}
}

@Throws(IOException::class)
private fun requireAllowedFileUri(context: Context, uri: Uri) {
val path = uri.path ?: throw IllegalArgumentException("Attachment file URI has no path")
val candidate = File(path).canonicalFile
val allowedRoots = listOf(
File(context.cacheDir, Constants.DRAFT_CACHE_DIR).canonicalFile,
File(context.cacheDir, Constants.ATTACHMENTS_CACHE_DIR).canonicalFile
)

if (allowedRoots.none { candidate.isInOrUnder(it) }) {
throw IllegalArgumentException("Attachment file URI points outside of FlowCrypt cache")
}
}

private fun File.isInOrUnder(root: File): Boolean {
return path == root.path || path.startsWith(root.path + File.separator)
}
}
Loading