diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/javamail/AttachmentInfoDataSource.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/javamail/AttachmentInfoDataSource.kt index 7284e61b67..9be1972824 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/javamail/AttachmentInfoDataSource.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/javamail/AttachmentInfoDataSource.kt @@ -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 @@ -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 @@ -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() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt index f1e57e0bb2..424e2d05d0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt @@ -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 @@ -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) @@ -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 ) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreateMessageActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreateMessageActivity.kt index aa7b79d8e1..0b4c052451 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreateMessageActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/CreateMessageActivity.kt @@ -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 @@ -73,16 +74,39 @@ class CreateMessageActivity : BaseActivity(), } 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 @@ -111,6 +135,25 @@ class CreateMessageActivity : BaseActivity(), } 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, @@ -121,11 +164,14 @@ class CreateMessageActivity : BaseActivity(), ): 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 } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/CreateOutgoingMessageDialogFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/CreateOutgoingMessageDialogFragment.kt index e387bdf680..e9c9fab2f5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/CreateOutgoingMessageDialogFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/dialog/CreateOutgoingMessageDialogFragment.kt @@ -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 @@ -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 @@ -43,6 +44,8 @@ class CreateOutgoingMessageDialogFragment : BaseDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + toast("SENDING!!!!") + isCancelable = false collectCreateOutgoingMessageStateFlow() createOutgoingMessageViewModel.create() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/OutgoingAttachmentUriValidator.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/OutgoingAttachmentUriValidator.kt new file mode 100644 index 0000000000..09f4096b27 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/OutgoingAttachmentUriValidator.kt @@ -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) + } +}