Show Encryption Authenticity warnings on messages in the timeline. (#3051)

* Initial implementation.

* Add developer option for showing timeline item authenticity.

* Refactor code to use new SendInfo.Status.

---------

Co-authored-by: Doug <douglase@element.io>
This commit is contained in:
Valere
2024-08-05 13:15:38 +02:00
committed by GitHub
parent cdaa88efcc
commit a11faeb131
29 changed files with 288 additions and 53 deletions
@@ -0,0 +1,74 @@
//
// Copyright 2024 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import MatrixRustSDK
/// Represents and issue with a timeline item's authenticity such as coming from an
/// unsigned session or being sent unencrypted in an encrypted room. See Rust's
/// `ShieldStateCode` for more information about the meaning of the cases.
enum EncryptionAuthenticity: Hashable {
enum Color { case red, gray }
case notGuaranteed(color: Color)
case unknownDevice(color: Color)
case unsignedDevice(color: Color)
case unverifiedIdentity(color: Color)
case sentInClear(color: Color)
var message: String {
switch self {
case .notGuaranteed:
L10n.eventShieldReasonAuthenticityNotGuaranteed
case .unknownDevice:
L10n.eventShieldReasonUnknownDevice
case .unsignedDevice:
L10n.eventShieldReasonUnsignedDevice
case .unverifiedIdentity:
L10n.eventShieldReasonUnverifiedIdentity
case .sentInClear:
L10n.eventShieldReasonSentInClear
}
}
}
extension EncryptionAuthenticity {
init?(shieldState: ShieldState) {
switch shieldState {
case .red(let code, _):
self.init(shieldStateCode: code, color: .red)
case .grey(let code, _):
self.init(shieldStateCode: code, color: .gray)
case .none:
return nil
}
}
init(shieldStateCode: ShieldStateCode, color: EncryptionAuthenticity.Color) {
switch shieldStateCode {
case .authenticityNotGuaranteed:
self = .notGuaranteed(color: color)
case .unknownDevice:
self = .unknownDevice(color: color)
case .unsignedDevice:
self = .unsignedDevice(color: color)
case .unverifiedIdentity:
self = .unverifiedIdentity(color: color)
case .sentInClear:
self = .sentInClear(color: color)
}
}
}
@@ -24,6 +24,8 @@ struct RoomTimelineItemProperties: Hashable {
var reactions: [AggregatedReaction] = []
/// The delivery status for this item. If a sent message is echoed the value is nil.
var deliveryStatus: TimelineItemDeliveryStatus?
/// The read receipts of the item, ordered from newest to oldest
/// The read receipts of the item, ordered from newest to oldest.
var orderedReadReceipts: [ReadReceipt] = []
/// Authenticity warnings for item's sent in encrypted rooms.
var encryptionAuthenticity: EncryptionAuthenticity?
}
@@ -120,7 +120,9 @@ class EventTimelineItemProxy {
let debugInfo = item.debugInfo()
return TimelineItemDebugInfo(model: debugInfo.model, originalJSON: debugInfo.originalJson, latestEditJSON: debugInfo.latestEditJson)
}()
lazy var shieldState = item.getShield(strict: false)
lazy var readReceipts = item.readReceipts()
}
@@ -50,7 +50,11 @@ extension EventBasedTimelineItemProtocol {
var pollIfAvailable: Poll? {
(self as? PollRoomTimelineItem)?.poll
}
var hasStatusIcon: Bool {
hasFailedToSend || properties.encryptionAuthenticity != nil
}
var hasFailedToSend: Bool {
properties.deliveryStatus == .sendingFailed
}
@@ -74,8 +78,8 @@ extension EventBasedTimelineItemProtocol {
whiteSpaces += 1
}
// To account for the extra spacing created by the alert icon
if hasFailedToSend {
// To account for the extra spacing created by the status icon
if hasStatusIcon {
whiteSpaces += 3
}
@@ -24,11 +24,14 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
/// The Matrix ID of the current user.
private let userID: String
private let encryptionAuthenticityEnabled: Bool
init(userID: String,
encryptionAuthenticityEnabled: Bool,
attributedStringBuilder: AttributedStringBuilderProtocol,
stateEventStringBuilder: RoomStateEventStringBuilder) {
self.userID = userID
self.encryptionAuthenticityEnabled = encryptionAuthenticityEnabled
self.attributedStringBuilder = attributedStringBuilder
self.stateEventStringBuilder = stateEventStringBuilder
}
@@ -156,7 +159,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
blurhash: imageInfo.blurhash,
properties: RoomTimelineItemProperties(reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildEncryptedTimelineItem(_ eventItemProxy: EventTimelineItemProxy,
@@ -220,7 +224,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildImageTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -240,7 +245,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildVideoTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -260,7 +266,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildAudioTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -280,7 +287,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildVoiceTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -300,7 +308,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildFileTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -320,7 +329,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildNoticeTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -340,7 +350,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildEmoteTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -360,7 +371,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildLocationTimelineItem(for eventItemProxy: EventTimelineItemProxy,
@@ -380,7 +392,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: messageTimelineItem.isEdited(),
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
// swiftlint:disable:next function_parameter_count
@@ -429,7 +442,8 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
properties: RoomTimelineItemProperties(isEdited: edited,
reactions: aggregateReactions(eventItemProxy.reactions),
deliveryStatus: eventItemProxy.deliveryStatus,
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts)))
orderedReadReceipts: orderReadReceipts(eventItemProxy.readReceipts),
encryptionAuthenticity: authenticity(eventItemProxy.shieldState)))
}
private func buildCallInviteTimelineItem(for eventItemProxy: EventTimelineItemProxy) -> RoomTimelineItemProtocol {
@@ -488,6 +502,11 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol {
}
}
private func authenticity(_ shieldState: ShieldState?) -> EncryptionAuthenticity? {
guard encryptionAuthenticityEnabled else { return nil }
return shieldState.flatMap(EncryptionAuthenticity.init)
}
// MARK: - Message events content
private func buildTextTimelineItemContent(_ messageContent: TextMessageContent) -> TextRoomTimelineItemContent {