diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt index 6171b2633b..00df238ab6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/RoomSummary.kt @@ -46,6 +46,7 @@ data class RoomSummary constructor( val readMarkerId: String? = null, val userDrafts: List = emptyList(), val isEncrypted: Boolean, + val encryptionEventTs: Long?, val inviterId: String? = null, val typingRoomMemberIds: List = emptyList(), val breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt index 2f3cdb9545..20651069b0 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/RoomSummaryMapper.kt @@ -53,6 +53,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa canonicalAlias = roomSummaryEntity.canonicalAlias, aliases = roomSummaryEntity.aliases.toList(), isEncrypted = roomSummaryEntity.isEncrypted, + encryptionEventTs = roomSummaryEntity.encryptionEventTs, typingRoomMemberIds = roomSummaryEntity.typingUserIds.toList(), breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt index 7009e762fb..5236cd26e6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/RoomSummaryEntity.kt @@ -48,6 +48,7 @@ internal open class RoomSummaryEntity( // this is required for querying var flatAliases: String = "", var isEncrypted: Boolean = false, + var encryptionEventTs: Long? = 0, var typingUserIds: RealmList = RealmList(), var roomEncryptionTrustLevelStr: String? = null, var inviterId: String? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index f4886c72da..54c8e33bce 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -136,6 +136,7 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.isEncrypted = encryptionEvent != null + roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs ?: System.currentTimeMillis() roomSummaryEntity.typingUserIds.clear() roomSummaryEntity.typingUserIds.addAll(ephemeralResult?.typingUserIds.orEmpty()) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt index 4391009b08..9e2a6c0f05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/composer/TextComposerView.kt @@ -26,7 +26,6 @@ import android.widget.ImageView import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import androidx.core.content.ContextCompat import androidx.core.text.toSpannable import androidx.core.view.isVisible import androidx.transition.AutoTransition @@ -172,7 +171,7 @@ class TextComposerView @JvmOverloads constructor(context: Context, attrs: Attrib RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning else -> R.drawable.ic_shield_black } - composerShieldImageView.setImageDrawable(ContextCompat.getDrawable(context, shieldRes)) + composerShieldImageView.setImageResource(shieldRes) } else { composerEditText.setHint(R.string.room_message_placeholder) composerShieldImageView.isVisible = false diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index d2cdf37eab..aa1119b283 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -29,6 +29,7 @@ import im.vector.riotx.core.epoxy.dividerItem import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration import im.vector.riotx.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.riotx.features.home.room.detail.timeline.tools.linkify import javax.inject.Inject @@ -72,6 +73,29 @@ class MessageActionsEpoxyController @Inject constructor( } } + when (state.informationData.e2eDecoration) { + E2EDecoration.WARN_IN_CLEAR -> { + bottomSheetSendStateItem { + id("e2e_clear") + showProgress(false) + text(stringProvider.getString(R.string.unencrytped)) + drawableStart(R.drawable.ic_shield_warning_small) + } + } + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + bottomSheetSendStateItem { + id("e2e_unverified") + showProgress(false) + text(stringProvider.getString(R.string.encrypted_unverified)) + drawableStart(R.drawable.ic_shield_warning_small) + } + } + else -> { + // nothing + } + } + // Quick reactions if (state.canReact() && state.quickStates is Success) { // Separator diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 0758e34495..2521e30907 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.detail.timeline.helper +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.toModel @@ -26,11 +27,13 @@ import im.vector.matrix.android.api.session.room.model.message.MessageVerificati import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.hasBeenEdited +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.getColorFromUserId +import im.vector.riotx.features.home.room.detail.timeline.item.E2EDecoration import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData @@ -72,6 +75,52 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses textColor = colorProvider.getColor(getColorFromUserId(event.root.senderId)) } + val room = event.root.roomId?.let { session.getRoom(it) } + val e2eDecoration: E2EDecoration + val isUserVerified = session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true + if (room?.isEncrypted() == true && isUserVerified) { + val ts = room.roomSummary()?.encryptionEventTs ?: 0 + val eventTs = event.root.originServerTs ?: 0 + if (event.isEncrypted()) { + // Do not decorate failed to decrypt, or redaction (we lost sender device info) + if (event.root.getClearType() == EventType.ENCRYPTED || event.root.isRedacted()) { + e2eDecoration = E2EDecoration.NONE + } else { + val sendingDevice = event.root.content + .toModel() + ?.deviceId + ?.let { deviceId -> + session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) + } + e2eDecoration = when { + sendingDevice == null -> { + // For now do not decorate this with warning + // maybe it's a deleted session + E2EDecoration.NONE + } + sendingDevice.trustLevel == null -> { + E2EDecoration.WARN_SENT_BY_UNKNOWN + } + sendingDevice.trustLevel?.isVerified().orFalse() -> { + E2EDecoration.NONE + } + else -> { + E2EDecoration.WARN_SENT_BY_UNVERIFIED + } + } + } + } else { + if (EventType.isStateEvent(event.root.type)) { + // Do not warn for state event, they are always in clear + e2eDecoration = E2EDecoration.NONE + } else { + // If event is in clear after the room enabled encryption we should warn + e2eDecoration = if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } + } + } else { + e2eDecoration = E2EDecoration.NONE + } return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -111,7 +160,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?: VerificationState.REQUEST ReferencesInfoData(verificationState) }, - sentByMe = event.root.senderId == session.myUserId + sentByMe = event.root.senderId == session.myUserId, + e2eDecoration = e2eDecoration ) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index 149b5e74ad..cccee05abd 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -21,6 +21,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import im.vector.matrix.android.api.session.room.send.SendState import im.vector.riotx.R @@ -92,6 +93,18 @@ abstract class AbsBaseMessageItem : BaseEventItem holder.reactionsContainer.setOnLongClickListener(baseAttributes.itemLongClickListener) } + when (baseAttributes.informationData.e2eDecoration) { + E2EDecoration.NONE -> { + holder.e2EDecorationView.isVisible = false + } + E2EDecoration.WARN_IN_CLEAR, + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + holder.e2EDecorationView.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning)) + holder.e2EDecorationView.isVisible = true + } + } + holder.view.setOnClickListener(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) } @@ -110,6 +123,7 @@ abstract class AbsBaseMessageItem : BaseEventItem abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { val reactionsContainer by bind(R.id.reactionsContainer) + val e2EDecorationView by bind(R.id.messageE2EDecoration) } /** diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index 8d4ae81201..088577d03a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -40,7 +40,8 @@ data class MessageInformationData( val hasPendingEdits: Boolean = false, val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, - val sentByMe : Boolean + val sentByMe : Boolean, + val e2eDecoration: E2EDecoration = E2EDecoration.NONE ) : Parcelable { val matrixItem: MatrixItem @@ -75,4 +76,11 @@ data class PollResponseData( val isClosed: Boolean = false ) : Parcelable +enum class E2EDecoration { + NONE, + WARN_IN_CLEAR, + WARN_SENT_BY_UNVERIFIED, + WARN_SENT_BY_UNKNOWN +} + fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt index ec98ea10ed..02cfc57bc7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/NoticeItem.kt @@ -19,6 +19,8 @@ package im.vector.riotx.features.home.room.detail.timeline.item import android.view.View import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -45,6 +47,18 @@ abstract class NoticeItem : BaseEventItem() { holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener) + + when (attributes.informationData.e2eDecoration) { + E2EDecoration.NONE -> { + holder.e2EDecorationView.isVisible = false + } + E2EDecoration.WARN_IN_CLEAR, + E2EDecoration.WARN_SENT_BY_UNVERIFIED, + E2EDecoration.WARN_SENT_BY_UNKNOWN -> { + holder.e2EDecorationView.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning)) + holder.e2EDecorationView.isVisible = true + } + } } override fun getEventIds(): List { @@ -56,6 +70,7 @@ abstract class NoticeItem : BaseEventItem() { class Holder : BaseHolder(STUB_ID) { val avatarImageView by bind(R.id.itemNoticeAvatarView) val noticeTextView by bind(R.id.itemNoticeTextView) + val e2EDecorationView by bind(R.id.messageE2EDecoration) } data class Attributes( diff --git a/vector/src/main/res/drawable/ic_shield_warning_small.xml b/vector/src/main/res/drawable/ic_shield_warning_small.xml new file mode 100644 index 0000000000..d42add32ea --- /dev/null +++ b/vector/src/main/res/drawable/ic_shield_warning_small.xml @@ -0,0 +1,20 @@ + + + + + diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index 77eaeae05c..cf43ed61af 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -62,6 +62,18 @@ android:layout_height="0dp" tools:layout_marginStart="52dp" /> + + + + + + + + Select your Recovery Key, or input it manually by typing it or pasting from your clipboard Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key. Failed to access secure storage + + Unencrypted + Encrypted by an unverified device