diff --git a/CHANGES.md b/CHANGES.md index 31a94730b5..b4ecac0bb1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,7 @@ Improvements 🙌: - Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719)) - Cross-Signing | Hide Use recovery key when 4S is not setup (#1007) - Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199 + - E2E timeline decoration (#1279) - Manage Session Settings / Cross Signing update (#1295) - Cross-Signing | Review sessions toast update old vs new (#1293, #1306) 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 8804f98976..1ad6112f2c 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/crypto/store/db/model/OlmInboundGroupSessionEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt index caa8cb9668..763e852cd1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/store/db/model/OlmInboundGroupSessionEntity.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.crypto.store.db.model +import im.vector.matrix.android.api.extensions.tryThis import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper import im.vector.matrix.android.internal.crypto.store.db.deserializeFromRealm import im.vector.matrix.android.internal.crypto.store.db.serializeForRealm @@ -36,7 +37,7 @@ internal open class OlmInboundGroupSessionEntity( : RealmObject() { fun getInboundGroupSession(): OlmInboundGroupSessionWrapper? { - return deserializeFromRealm(olmInboundGroupSessionData) + return tryThis { deserializeFromRealm(olmInboundGroupSessionData) } } fun putInboundGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper?) { 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 7044b3b47d..6e0adccfb9 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 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..e77d9ec73f 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.unencrypted)) + 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..6b44b9f3d3 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,19 +18,23 @@ 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 +import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent 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 +76,8 @@ 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 = getE2EDecoration(room, event) return MessageInformationData( eventId = eventId, senderId = event.root.senderId ?: "", @@ -111,10 +117,59 @@ 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 ) } + private fun getE2EDecoration(room: Room?, event: TimelineEvent): E2EDecoration { + return if (room?.isEncrypted() == true + // is user verified + && session.cryptoService().crossSigningService().getUserCrossSigningKeys(event.root.senderId ?: "")?.isTrusted() == true) { + 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.NONE + } else { + val sendingDevice = event.root.content + .toModel() + ?.deviceId + ?.let { deviceId -> + session.cryptoService().getDeviceInfo(event.root.senderId ?: "", deviceId) + } + 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.NONE + } else { + // If event is in clear after the room enabled encryption we should warn + if (eventTs > ts) E2EDecoration.WARN_IN_CLEAR else E2EDecoration.NONE + } + } + } else { + E2EDecoration.NONE + } + } + /** * Tiles type message never show the sender information (like verification request), so we should repeat it for next message * even if same sender 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..e62de05518 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 @@ -92,6 +92,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.setImageResource(R.drawable.ic_shield_warning) + holder.e2EDecorationView.isVisible = true + } + } + holder.view.setOnClickListener(baseAttributes.itemClickListener) holder.view.setOnLongClickListener(baseAttributes.itemLongClickListener) } @@ -110,6 +122,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..a4d5d273e7 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,7 @@ 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.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.riotx.R @@ -45,6 +46,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.setImageResource(R.drawable.ic_shield_warning) + holder.e2EDecorationView.isVisible = true + } + } } override fun getEventIds(): List { @@ -56,6 +69,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..3ae80424cc 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,24 @@ android:layout_height="0dp" tools:layout_marginStart="52dp" /> + + + + + + + + + + + Use the latest Riot on your other devices, Riot Web, Riot Desktop, Riot iOS, RiotX for Android, or another cross-signing capable Matrix client + Riot Web\nRiot Desktop + Riot iOS\nRiot X for Android + or another cross-signing capable Matrix client + Use the latest Riot on your other devices: + Forces the current outbound group session in an encrypted room to be discarded + Only supported in encrypted rooms + + Use your %1$s or use your %2$s to continue. + Use Recovery Key + 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 Review where you’re logged in Verify all your sessions to ensure your account & messages are safe