Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

App: UISI AutoReporting #5743

Merged
merged 10 commits into from
Mar 24, 2022
2 changes: 1 addition & 1 deletion Config/BuildSettings.swift
Original file line number Diff line number Diff line change
@@ -378,7 +378,7 @@ final class BuildSettings: NSObject {
static let secretsRecoveryAllowReset = true

// MARK: - UISI Autoreporting
static let cryptoUISIAutoReportingEnabled = true
static let cryptoUISIAutoReportingEnabled = false

// MARK: - Polls

1 change: 1 addition & 0 deletions Riot/Assets/en.lproj/Vector.strings
Original file line number Diff line number Diff line change
@@ -619,6 +619,7 @@ Tap the + to start adding people.";
"settings_labs_enable_ringing_for_group_calls" = "Ring for group calls";
"settings_labs_enabled_polls" = "Polls";
"settings_labs_enable_threads" = "Threaded messaging";
"settings_labs_enable_auto_report_decryption_errors" = "Auto Report Decryption Errors";

"settings_version" = "Version %@";
"settings_olm_version" = "Olm Version %@";
4 changes: 4 additions & 0 deletions Riot/Generated/Strings.swift
Original file line number Diff line number Diff line change
@@ -4919,6 +4919,10 @@ public class VectorL10n: NSObject {
public static var settingsLabsE2eEncryptionPromptMessage: String {
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message")
}
/// Auto Report Decryption Errors
public static var settingsLabsEnableAutoReportDecryptionErrors: String {
return VectorL10n.tr("Vector", "settings_labs_enable_auto_report_decryption_errors")
}
/// Ring for group calls
public static var settingsLabsEnableRingingForGroupCalls: String {
return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls")
22 changes: 9 additions & 13 deletions Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift
Original file line number Diff line number Diff line change
@@ -23,7 +23,6 @@ struct UISIAutoReportData {
let roomId: String?
let senderKey: String?
let deviceId: String?
let source: UISIEventSource?
let userId: String?
let sessionId: String?
}
@@ -34,7 +33,6 @@ extension UISIAutoReportData: Codable {
case roomId = "room_id"
case senderKey = "sender_key"
case deviceId = "device_id"
case source
case userId = "user_id"
case sessionId = "session_id"
}
@@ -48,13 +46,14 @@ extension UISIAutoReportData: Codable {
let sessionId: String
}

static let autoRsRequest = "im.vector.auto_rs_request"
private static let autoRsRequest = "im.vector.auto_rs_request"
private static let reportSpacing = 60

private let bugReporter: MXBugReportRestClient
private let dispatchQueue = DispatchQueue(label: "io.element.UISIAutoReporter.queue")
// Simple in memory cache of already sent report
private var alreadyReportedUisi = Set<ReportInfo>()
private let e2eDetectedSubject = PassthroughSubject<E2EMessageDetected, Never>()
private let e2eDetectedSubject = PassthroughSubject<UISIDetectedMessage, Never>()
private let matchingRSRequestSubject = PassthroughSubject<MXEvent, Never>()
private var cancellables = Set<AnyCancellable>()
private var sessions = [MXSession]()
@@ -70,14 +69,14 @@ extension UISIAutoReportData: Codable {
super.init()
// Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests.
e2eDetectedSubject
.bufferAndSpace(spacingDelay: 2)
.bufferAndSpace(spacingDelay: Self.reportSpacing)
.sink { [weak self] in
guard let self = self else { return }
self.sendRageShake(source: $0)
}.store(in: &cancellables)

matchingRSRequestSubject
.bufferAndSpace(spacingDelay: 2)
.bufferAndSpace(spacingDelay: Self.reportSpacing)
.sink { [weak self] in
guard let self = self else { return }
self.sendMatchingRageShake(source: $0)
@@ -103,8 +102,7 @@ extension UISIAutoReportData: Codable {
return Self.autoRsRequest
}

func uisiDetected(source: E2EMessageDetected) {
guard source.source != UISIEventSource.initialSync else { return }
func uisiDetected(source: UISIDetectedMessage) {
dispatchQueue.async {
let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId)
let alreadySent = self.alreadyReportedUisi.contains(reportInfo)
@@ -120,15 +118,14 @@ extension UISIAutoReportData: Codable {
self.matchingRSRequestSubject.send(source)
}

func sendRageShake(source: E2EMessageDetected) {
MXLog.debug("dl sendRageShake")
func sendRageShake(source: UISIDetectedMessage) {
MXLog.debug("[UISIAutoReporter] sendRageShake")
guard let session = sessions.first else { return }
let uisiData = UISIAutoReportData(
eventId: source.eventId,
roomId: source.roomId,
senderKey: source.senderKey,
deviceId: source.senderDeviceId,
source: source.source,
userId: source.senderUserId,
sessionId: source.sessionId
).jsonString ?? ""
@@ -174,7 +171,7 @@ extension UISIAutoReportData: Codable {
}

func sendMatchingRageShake(source: MXEvent) {
MXLog.debug("dl sendMatchingRageShake")
MXLog.debug("[UISIAutoReporter] sendMatchingRageShake")
let eventId = source.content["event_id"] as? String
let roomId = source.content["room_id"] as? String
let sessionId = source.content["session_id"] as? String
@@ -189,7 +186,6 @@ extension UISIAutoReportData: Codable {
roomId: roomId,
senderKey: senderKey,
deviceId: deviceId,
source: nil,
userId: userId,
sessionId: sessionId
).jsonString ?? ""
124 changes: 41 additions & 83 deletions Riot/Managers/UISIAutoReporter/UISIDetector.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,132 +19,90 @@ import Foundation

protocol UISIDetectorDelegate: AnyObject {
var reciprocateToDeviceEventType: String { get }
func uisiDetected(source: E2EMessageDetected)
func uisiDetected(source: UISIDetectedMessage)
func uisiReciprocateRequest(source: MXEvent)
}

enum UISIEventSource: String {
case initialSync = "INITIAL_SYNC"
case incrementalSync = "INCREMENTAL_SYNC"
case pagination = "PAGINATION"
}

extension UISIEventSource: Equatable, Codable { }

struct E2EMessageDetected {
struct UISIDetectedMessage {
let eventId: String
let roomId: String
let senderUserId: String
let senderDeviceId: String
let senderKey: String
let sessionId: String
let source: UISIEventSource

static func fromEvent(event: MXEvent, roomId: String, source: UISIEventSource) -> E2EMessageDetected {
return E2EMessageDetected(
static func fromEvent(event: MXEvent) -> UISIDetectedMessage {
return UISIDetectedMessage(
eventId: event.eventId ?? "",
roomId: roomId,
roomId: event.roomId,
senderUserId: event.sender,
senderDeviceId: event.wireContent["device_id"] as? String ?? "",
senderKey: event.wireContent["sender_key"] as? String ?? "",
sessionId: event.wireContent["session_id"] as? String ?? "",
source: source
sessionId: event.wireContent["session_id"] as? String ?? ""
)
}
}

extension E2EMessageDetected: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(eventId)
hasher.combine(roomId)
}
}


class UISIDetector: MXLiveEventListener {

gileluard marked this conversation as resolved.
Show resolved Hide resolved
weak var delegate: UISIDetectorDelegate?
var enabled = false

private var trackedEvents = [String: (E2EMessageDetected, DispatchSourceTimer)]()
var initialSyncCompleted = false
private var trackedUISIs = [String: DispatchSourceTimer]()
private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue")
private static let timeoutSeconds = 30


func onLiveEvent(roomId: String, event: MXEvent) {
guard enabled, event.isEncrypted, event.clear == nil else { return }
dispatchQueue.async {
self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .incrementalSync))
}
}

func onPaginatedEvent(roomId: String, event: MXEvent) {
guard enabled, event.isEncrypted, event.clear == nil else { return }
dispatchQueue.async {
self.handleEventReceived(detectorEvent: E2EMessageDetected.fromEvent(event: event, roomId: roomId, source: .pagination))
}
}
private static let gracePeriodSeconds = 30

gileluard marked this conversation as resolved.
Show resolved Hide resolved
func onEventDecrypted(eventId: String, roomId: String, clearEvent: [AnyHashable: Any]) {
guard enabled else { return }
func onSessionStateChanged(state: MXSessionState) {
dispatchQueue.async {
self.unTrack(eventId: eventId, roomId: roomId)
self.initialSyncCompleted = state == .running
}
}

func onEventDecryptionError(eventId: String, roomId: String, error: Error) {
guard enabled else { return }
func onLiveEventDecryptionAttempted(event: MXEvent, result: MXEventDecryptionResult) {
guard enabled, let eventId = event.eventId, let roomId = event.roomId else { return }
dispatchQueue.async {
if let event = self.unTrack(eventId: eventId, roomId: roomId) {
self.triggerUISI(source: event)
let trackedId = Self.trackedEventId(roomId: eventId, eventId: roomId)

if let timer = self.trackedUISIs[trackedId],
result.clearEvent != nil {
// successfully decrypted during grace period, cancel timer.
self.trackedUISIs[trackedId] = nil
timer.cancel()
return
}

guard self.initialSyncCompleted,
result.clearEvent == nil
else { return }

// track uisi and report it only if it is not decrypted before grade period ends
let timer = DispatchSource.makeTimerSource(queue: self.dispatchQueue)
timer.schedule(deadline: .now() + .seconds(Self.gracePeriodSeconds))
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.trackedUISIs[trackedId] = nil
MXLog.verbose("[UISIDetector] onLiveEventDecryptionAttempted: Timeout on \(eventId)")
self.triggerUISI(source: UISIDetectedMessage.fromEvent(event: event))
}
self.trackedUISIs[trackedId] = timer
timer.activate()
}

}

func onLiveToDeviceEvent(event: MXEvent) {
guard enabled, event.type == delegate?.reciprocateToDeviceEventType else { return }
delegate?.uisiReciprocateRequest(source: event)
}

gileluard marked this conversation as resolved.
Show resolved Hide resolved
private func handleEventReceived(detectorEvent: E2EMessageDetected) {
guard enabled else { return }
let trackedId = Self.trackedEventId(roomId: detectorEvent.roomId, eventId: detectorEvent.eventId)
guard trackedEvents[trackedId] == nil else {
MXLog.warning("## UISIDetector: Event \(detectorEvent.eventId) is already tracked")
return
}
// track it and start timer
let timer = DispatchSource.makeTimerSource(queue: dispatchQueue)
timer.schedule(deadline: .now() + .seconds(Self.timeoutSeconds))
timer.setEventHandler { [weak self] in
guard let self = self else { return }
self.unTrack(eventId: detectorEvent.eventId, roomId: detectorEvent.roomId)
MXLog.verbose("## UISIDetector: Timeout on \(detectorEvent.eventId)")
self.triggerUISI(source: detectorEvent)
}
trackedEvents[trackedId] = (detectorEvent, timer)
timer.activate()
}

private func triggerUISI(source: E2EMessageDetected) {
private func triggerUISI(source: UISIDetectedMessage) {
guard enabled else { return }
MXLog.info("## UISIDetector: Unable To Decrypt \(source)")
MXLog.info("[UISIDetector] triggerUISI: Unable To Decrypt \(source)")
self.delegate?.uisiDetected(source: source)
}

@discardableResult private func unTrack(eventId: String, roomId: String) -> E2EMessageDetected? {
let trackedId = Self.trackedEventId(roomId: roomId, eventId: eventId)
guard let (event, timer) = trackedEvents[trackedId]
else {
return nil
}
trackedEvents[trackedId] = nil
timer.cancel()
return event
}

gileluard marked this conversation as resolved.
Show resolved Hide resolved
static func trackedEventId(roomId: String, eventId: String) -> String {
return "\(roomId)-\(eventId)"
}

}
29 changes: 28 additions & 1 deletion Riot/Modules/Settings/SettingsViewController.m
Original file line number Diff line number Diff line change
@@ -159,7 +159,8 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
{
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
LABS_ENABLE_THREADS_INDEX,
LABS_ENABLE_MESSAGE_BUBBLES_INDEX
LABS_ENABLE_MESSAGE_BUBBLES_INDEX,
LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS
};

typedef NS_ENUM(NSUInteger, SECURITY)
@@ -572,6 +573,7 @@ - (void)updateSections
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_MESSAGE_BUBBLES_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS];
sectionLabs.headerTitle = [VectorL10n settingsLabs];
if (sectionLabs.hasAnyRows)
{
@@ -1490,6 +1492,21 @@ - (UITableViewCell *)buildMessageBubblesCellForTableView:(UITableView*)tableView
return labelAndSwitchCell;
}

- (UITableViewCell *)buildAutoReportDecryptionErrorsCellForTableView:(UITableView*)tableView
atIndexPath:(NSIndexPath*)indexPath
{
MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];

labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableAutoReportDecryptionErrors];

labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableUISIAutoReporting;
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
labelAndSwitchCell.mxkSwitch.enabled = YES;
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableAutoReportDecryptionErrors:) forControlEvents:UIControlEventTouchUpInside];

return labelAndSwitchCell;
}

#pragma mark - 3Pid Add

- (void)showAuthenticationIfNeededForAdding:(MX3PIDMedium)medium withSession:(MXSession*)session completion:(void (^)(NSDictionary* authParams))completion
@@ -2462,6 +2479,10 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N
{
cell = [self buildMessageBubblesCellForTableView:tableView atIndexPath:indexPath];
}
else if (row == LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS)
{
cell = [self buildAutoReportDecryptionErrorsCellForTableView:tableView atIndexPath:indexPath];
}
}
else if (section == SECTION_TAG_FLAIR)
{
@@ -3890,6 +3911,12 @@ - (void)toggleEnableRoomMessageBubbles:(UISwitch *)sender
[roomDataSourceManager reset];
}


- (void)toggleEnableAutoReportDecryptionErrors:(UISwitch *)sender
{
RiotSettings.shared.enableUISIAutoReporting = sender.isOn;
}

#pragma mark - TextField listener

- (IBAction)textFieldDidChange:(id)sender