diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index b9c639f158..e4659c1499 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -191,6 +191,7 @@ final class BuildSettings: NSObject { static let bugReportEndpointUrlString = "https://riot.im/bugreports" // Use the name allocated by the bug report server static let bugReportApplicationId = "riot-ios" + static let bugReportUISIId = "element-auto-uisi" // MARK: - Integrations @@ -377,6 +378,9 @@ final class BuildSettings: NSObject { // MARK: - Secrets Recovery static let secretsRecoveryAllowReset = true + // MARK: - UISI Autoreporting + static let cryptoUISIAutoReportingEnabled = false + // MARK: - Polls static var pollsEnabled: Bool { diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index af5aa32bea..15bcb489b1 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -629,6 +629,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_labs_use_only_latest_user_avatar_and_name" = "Show latest avatar and name for users in message history"; "settings_version" = "Version %@"; @@ -2424,7 +2425,7 @@ Tap the + to start adding people."; "language_picker_title" = "Choose a language"; "language_picker_default_language" = "Default (%@)"; -/* -*- +/* -*- Automatic localization for en The following key/value pairs were extracted from the android i18n file: @@ -2507,17 +2508,17 @@ Tap the + to start adding people."; "notice_room_history_visible_to_members_from_joined_point_by_you" = "You made future room history visible to all room members, from the point they joined."; "notice_room_history_visible_to_members_from_joined_point_by_you_for_dm" = "You made future messages visible to everyone, from when they joined."; -// Room Screen +// Room Screen -// general errors +// general errors -// Home Screen +// Home Screen -// Last seen time +// Last seen time -// call events +// call events -/* -*- +/* -*- Automatic localization for en The following key/value pairs were extracted from the android i18n file: @@ -2525,9 +2526,9 @@ Tap the + to start adding people."; */ -// titles +// titles -// button names +// button names "send" = "Send"; "copy_button_name" = "Copy"; "resend" = "Resend"; @@ -2535,7 +2536,7 @@ Tap the + to start adding people."; "share" = "Share"; "delete" = "Delete"; -// actions +// actions "action_logout" = "Logout"; "create_room" = "Create Room"; "login" = "Login"; @@ -2550,32 +2551,32 @@ Tap the + to start adding people."; "unban" = "Un-ban"; "message_unsaved_changes" = "There are unsaved changes. Leaving will discard them."; -// Login Screen +// Login Screen "login_error_already_logged_in" = "Already logged in"; "login_error_must_start_http" = "URL must start with http[s]://"; -// members list Screen +// members list Screen -// accounts list Screen +// accounts list Screen -// image size selection +// image size selection -// invitation members list Screen +// invitation members list Screen -// room creation dialog Screen +// room creation dialog Screen // room info dialog Screen // room details dialog screen -// contacts list screen +// contacts list screen "invitation_message" = "I\'d like to chat with you with matrix. Please, visit the website http://matrix.org to have more information."; -// Settings screen +// Settings screen "settings_title_config" = "Configuration"; "settings_title_notifications" = "Notifications"; -// Notification settings screen +// Notification settings screen "notification_settings_disable_all" = "Disable all notifications"; "notification_settings_enable_notifications" = "Enable notifications"; "notification_settings_enable_notifications_warning" = "All notifications are currently disabled for all devices."; @@ -2602,10 +2603,10 @@ Tap the + to start adding people."; "notification_settings_by_default" = "By default..."; "notification_settings_notify_all_other" = "Notify for all other messages/rooms"; -// gcm section +// gcm section "settings_config_identity_server" = "Identity server: %@"; -// Settings keys +// Settings keys // call string "call_connecting" = "Connecting…"; @@ -2638,4 +2639,3 @@ Tap the + to start adding people."; "ssl_unexpected_existing_expl" = "The certificate has changed from one that was trusted by your phone. This is HIGHLY UNUSUAL. It is recommended that you DO NOT ACCEPT this new certificate."; "ssl_expected_existing_expl" = "The certificate has changed from a previously trusted one to one that is not trusted. The server may have renewed its certificate. Contact the server administrator for the expected fingerprint."; "ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above."; - diff --git a/Riot/Categories/Codable.swift b/Riot/Categories/Codable.swift new file mode 100644 index 0000000000..b27c9c7ee0 --- /dev/null +++ b/Riot/Categories/Codable.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 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 + +extension Encodable { + /// Convenience method to get the json string of an Encodable + var jsonString: String? { + let encoder = JSONEncoder() + guard let jsonData = try? encoder.encode(self) else { return nil } + return String(data: jsonData, encoding: .utf8) + } +} diff --git a/Riot/Categories/MXBugReportRestClient+Riot.swift b/Riot/Categories/MXBugReportRestClient+Riot.swift new file mode 100644 index 0000000000..93eff76aaa --- /dev/null +++ b/Riot/Categories/MXBugReportRestClient+Riot.swift @@ -0,0 +1,114 @@ +// +// Copyright 2021 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 MatrixSDK +import GBDeviceInfo + +extension MXBugReportRestClient { + + @objc static func vc_bugReportRestClient(appName: String) -> MXBugReportRestClient { + let client = MXBugReportRestClient(bugReportEndpoint: BuildSettings.bugReportEndpointUrlString) + // App info + client.appName = appName + client.version = AppDelegate.theDelegate().appVersion + client.build = AppDelegate.theDelegate().build + + client.deviceModel = GBDeviceInfo.deviceInfo().modelString + client.deviceOS = "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" + return client + } + + @objc func vc_sendBugReport( + description: String, + sendLogs: Bool, + sendCrashLog: Bool, + sendFiles: [URL]? = nil, + additionalLabels: [String]? = nil, + customFields: [String: String]? = nil, + progress: ((MXBugReportState, Progress?) -> Void)? = nil, + success: ((String?) -> Void)? = nil, + failure: ((Error?) -> Void)? = nil + ) { + // User info (TODO: handle multi-account and find a way to expose them in rageshake API) + var userInfo = [String: String]() + let mainAccount = MXKAccountManager.shared().accounts.first + if let userId = mainAccount?.mxSession.myUser.userId { + userInfo["user_id"] = userId + } + if let deviceId = mainAccount?.mxSession.matrixRestClient.credentials.deviceId { + userInfo["device_id"] = deviceId + } + + userInfo["locale"] = NSLocale.preferredLanguages[0] + userInfo["default_app_language"] = Bundle.main.preferredLocalizations[0] // The language chosen by the OS + userInfo["app_language"] = Bundle.mxk_language() ?? userInfo["default_app_language"] // The language chosen by the user + + // Application settings + userInfo["lazy_loading"] = MXKAppSettings.standard().syncWithLazyLoadOfRoomMembers ? "ON" : "OFF" + + let currentDate = Date() + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + userInfo["local_time"] = dateFormatter.string(from: currentDate) + + dateFormatter.timeZone = TimeZone(identifier: "UTC") + userInfo["utc_time"] = dateFormatter.string(from: currentDate) + + if let customFields = customFields { + // combine userInfo with custom fields overriding with custom where there is a conflict + userInfo.merge(customFields) { (_, new) in new } + } + others = userInfo + + var labels: [String] = additionalLabels ?? [String]() + // Add a Github label giving information about the version + if var versionLabel = version, let buildLabel = build { + + // If this is not the app store version, be more accurate on the build origin + if buildLabel == VectorL10n.settingsConfigNoBuildInfo { + // This is a debug session from Xcode + versionLabel += "-debug" + } else if !buildLabel.contains("master") { + // This is a Jenkins build. Add the branch and the build number + let buildString = buildLabel.replacingOccurrences(of: " ", with: "-") + versionLabel += "-\(buildString)" + } + labels += [versionLabel] + } + if sendCrashLog { + labels += ["crash"] + } + + var sendDescription = description + if sendCrashLog, + let crashLogFile = MXLogger.crashLog(), + let crashLog = try? String(contentsOfFile: crashLogFile, encoding: .utf8) { + // Append the crash dump to the user description in order to ease triaging of GH issues + sendDescription += "\n\n\n--------------------------------------------------------------------------------\n\n\(crashLog)" + } + + sendBugReport(sendDescription, + sendLogs: sendLogs, + sendCrashLog: sendCrashLog, + sendFiles: sendFiles, + attachGitHubLabels: labels, + progress: progress, + success: success, + failure: failure) + } + +} diff --git a/Riot/Categories/Publisher+Riot.swift b/Riot/Categories/Publisher+Riot.swift new file mode 100644 index 0000000000..6fa9e20510 --- /dev/null +++ b/Riot/Categories/Publisher+Riot.swift @@ -0,0 +1,37 @@ +// +// Copyright 2021 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 Combine + +@available(iOS 14.0, *) +extension Publisher { + + /// + /// Buffer upstream items and guarantee a time interval spacing out the published items. + /// - Parameters: + /// - spacingDelay: A delay in seconds to guarantee between emissions + /// - scheduler: The `DispatchQueue` on which to schedule emissions. + /// - Returns: The new wrapped publisher + func bufferAndSpace(spacingDelay: Int, scheduler: DispatchQueue = DispatchQueue.main) -> Publishers.FlatMap< + Publishers.SetFailureType.Output>, DispatchQueue>, Publishers.Buffer.Failure>, + Publishers.Buffer + > { + return buffer(size: .max, prefetch: .byRequest, whenFull: .dropNewest) + .flatMap(maxPublishers: .max(1)) { + Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler) + } + } +} diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index ca1a6125a1..e48f2d7b2f 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -6667,6 +6667,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") diff --git a/Riot/Managers/Settings/RiotSettings+Publisher.swift b/Riot/Managers/Settings/RiotSettings+Publisher.swift new file mode 100644 index 0000000000..f1e6f17cb6 --- /dev/null +++ b/Riot/Managers/Settings/RiotSettings+Publisher.swift @@ -0,0 +1,29 @@ +// +// Copyright 2021 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 Combine + +extension RiotSettings { + + @available(iOS 13.0, *) + func publisher(for key: String) -> AnyPublisher { + return NotificationCenter.default.publisher(for: .userDefaultValueUpdated) + .filter({ $0.object as? String == key }) + .eraseToAnyPublisher() + } + +} diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 01a57f3c8d..f90021ba37 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -30,6 +30,7 @@ final class RiotSettings: NSObject { static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread" static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace" + static let enableUISIAutoReporting = "enableUISIAutoReporting" } static let shared = RiotSettings() @@ -146,6 +147,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) var enableThreads + /// Indicates if auto reporting of decryption errors is enabled + @UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults) + var enableUISIAutoReporting + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. diff --git a/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift new file mode 100644 index 0000000000..893dbbe298 --- /dev/null +++ b/Riot/Managers/UISIAutoReporter/UISIAutoReporter.swift @@ -0,0 +1,236 @@ +// +// Copyright 2021 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 MatrixSDK +import Combine + +struct UISIAutoReportData { + let eventId: String? + let roomId: String? + let senderKey: String? + let deviceId: String? + let userId: String? + let sessionId: String? +} + +extension UISIAutoReportData: Codable { + enum CodingKeys: String, CodingKey { + case eventId = "event_id" + case roomId = "room_id" + case senderKey = "sender_key" + case deviceId = "device_id" + case userId = "user_id" + case sessionId = "session_id" + } +} + + +/// Listens for failed decryption events and silently sends reports RageShake server. +/// Also requests that message senders send a matching report to have both sides of the interaction. +@available(iOS 14.0, *) +@objcMembers class UISIAutoReporter: NSObject, UISIDetectorDelegate { + + struct ReportInfo: Hashable { + let roomId: String + let sessionId: String + } + + // MARK: - Properties + + 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() + private let e2eDetectedSubject = PassthroughSubject() + private let matchingRSRequestSubject = PassthroughSubject() + private var cancellables = Set() + private var sessions = [MXSession]() + private var enabled = false { + didSet { + guard oldValue != enabled else { return } + detector.enabled = enabled + } + } + + // MARK: - Setup + + override init() { + self.bugReporter = MXBugReportRestClient.vc_bugReportRestClient(appName: BuildSettings.bugReportUISIId) + super.init() + // Simple rate limiting, for any rage-shakes emitted we guarantee a spacing between requests. + e2eDetectedSubject + .bufferAndSpace(spacingDelay: Self.reportSpacing) + .sink { [weak self] in + guard let self = self else { return } + self.sendRageShake(source: $0) + }.store(in: &cancellables) + + matchingRSRequestSubject + .bufferAndSpace(spacingDelay: Self.reportSpacing) + .sink { [weak self] in + guard let self = self else { return } + self.sendMatchingRageShake(source: $0) + }.store(in: &cancellables) + + self.enabled = RiotSettings.shared.enableUISIAutoReporting + RiotSettings.shared.publisher(for: RiotSettings.UserDefaultsKeys.enableUISIAutoReporting) + .sink { [weak self] _ in + guard let self = self else { return } + self.enabled = RiotSettings.shared.enableUISIAutoReporting + } + .store(in: &cancellables) + } + + private lazy var detector: UISIDetector = { + let detector = UISIDetector() + detector.delegate = self + return detector + }() + + var reciprocateToDeviceEventType: String { + return Self.autoRsRequest + } + + // MARK: - Public + + func uisiDetected(source: UISIDetectedMessage) { + dispatchQueue.async { + let reportInfo = ReportInfo(roomId: source.roomId, sessionId: source.sessionId) + let alreadySent = self.alreadyReportedUisi.contains(reportInfo) + if !alreadySent { + self.alreadyReportedUisi.insert(reportInfo) + self.e2eDetectedSubject.send(source) + } + } + } + + func add(_ session: MXSession) { + sessions.append(session) + detector.enabled = enabled + session.eventStreamService.add(eventStreamListener: detector) + } + + func remove(_ session: MXSession) { + if let index = sessions.firstIndex(of: session) { + sessions.remove(at: index) + } + session.eventStreamService.remove(eventStreamListener: detector) + } + + func uisiReciprocateRequest(source: MXEvent) { + guard source.type == Self.autoRsRequest else { return } + self.matchingRSRequestSubject.send(source) + } + + // MARK: - Private + + private 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, + userId: source.senderUserId, + sessionId: source.sessionId + ).jsonString ?? "" + + self.bugReporter.vc_sendBugReport( + description: "Auto-reporting decryption error", + sendLogs: true, + sendCrashLog: true, + additionalLabels: [ + "Z-UISI", + "ios", + "uisi-recipient" + ], + customFields: ["auto_uisi": uisiData], + success: { reportUrl in + let contentMap = MXUsersDevicesMap() + let content = [ + "event_id": source.eventId, + "room_id": source.roomId, + "session_id": source.sessionId, + "device_id": source.senderDeviceId, + "user_id": source.senderUserId, + "sender_key": source.senderKey, + "recipient_rageshake": reportUrl + ] + contentMap.setObject(content as NSDictionary, forUser: source.senderUserId, andDevice: source.senderDeviceId) + session.matrixRestClient.sendDirectToDevice( + eventType: Self.autoRsRequest, + contentMap: contentMap, + txnId: nil + ) { response in + if response.isFailure { + MXLog.warning("failed to send auto-uisi to device") + } + } + }, + failure: { [weak self] error in + guard let self = self else { return } + self.dispatchQueue.async { + self.alreadyReportedUisi.remove(ReportInfo(roomId: source.roomId, sessionId: source.sessionId)) + } + }) + } + + private func sendMatchingRageShake(source: MXEvent) { + 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 + let deviceId = source.content["device_id"] as? String + let userId = source.content["user_id"] as? String + let senderKey = source.content["sender_key"] as? String + let matchingIssue = source.content["recipient_rageshake"] as? String + + var description = "Auto-reporting decryption error (sender)" + if let matchingIssue = matchingIssue { + description += "\nRecipient rageshake: \(matchingIssue)" + } + + let uisiData = UISIAutoReportData( + eventId: eventId, + roomId: roomId, + senderKey: senderKey, + deviceId: deviceId, + userId: userId, + sessionId: sessionId + ).jsonString ?? "" + + self.bugReporter.vc_sendBugReport( + description: description, + sendLogs: true, + sendCrashLog: true, + additionalLabels: [ + "Z-UISI", + "ios", + "uisi-sender" + ], + customFields: [ + "auto_uisi": uisiData, + "recipient_rageshake": matchingIssue ?? "" + ] + ) + } + +} diff --git a/Riot/Managers/UISIAutoReporter/UISIDetector.swift b/Riot/Managers/UISIAutoReporter/UISIDetector.swift new file mode 100644 index 0000000000..5a701230f7 --- /dev/null +++ b/Riot/Managers/UISIAutoReporter/UISIDetector.swift @@ -0,0 +1,115 @@ +// +// Copyright 2021 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 MatrixSDK +import Foundation + +protocol UISIDetectorDelegate: AnyObject { + var reciprocateToDeviceEventType: String { get } + func uisiDetected(source: UISIDetectedMessage) + func uisiReciprocateRequest(source: MXEvent) +} + +struct UISIDetectedMessage { + let eventId: String + let roomId: String + let senderUserId: String + let senderDeviceId: String + let senderKey: String + let sessionId: String + + static func fromEvent(event: MXEvent) -> UISIDetectedMessage { + return UISIDetectedMessage( + eventId: event.eventId ?? "", + 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 ?? "" + ) + } +} + +/// Detects decryption errors that occur and don't recover within a grace period. +/// see `UISIDetectorDelegate` for listening to detections. +class UISIDetector: MXLiveEventListener { + + weak var delegate: UISIDetectorDelegate? + var enabled = false + + var initialSyncCompleted = false + private var trackedUISIs = [String: DispatchSourceTimer]() + private let dispatchQueue = DispatchQueue(label: "io.element.UISIDetector.queue") + private static let gracePeriodSeconds = 30 + + // MARK: - Public + + func onSessionStateChanged(state: MXSessionState) { + dispatchQueue.async { + self.initialSyncCompleted = state == .running + } + } + + func onLiveEventDecryptionAttempted(event: MXEvent, result: MXEventDecryptionResult) { + guard enabled, let eventId = event.eventId, let roomId = event.roomId else { return } + dispatchQueue.async { + 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) + } + + // MARK: - Private + + private func triggerUISI(source: UISIDetectedMessage) { + guard enabled else { return } + MXLog.info("[UISIDetector] triggerUISI: Unable To Decrypt \(source)") + self.delegate?.uisiDetected(source: source) + } + + // MARK: - Static + + private static func trackedEventId(roomId: String, eventId: String) -> String { + return "\(roomId)-\(eventId)" + } +} diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index e177e2a4a5..9b30b50556 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -220,6 +220,7 @@ @interface LegacyAppDelegate () *files; if (_screenshot && _sendScreenshot) @@ -347,56 +308,23 @@ - (IBAction)onSendButtonPress:(id)sender files = @[screenShotFile]; } - - // Prepare labels to attach to the GitHub issue - NSMutableArray *gitHubLabels = [NSMutableArray array]; - if (_reportCrash) - { - // Label the GH issue as "crash" - [gitHubLabels addObject:@"crash"]; - } - - // Add a Github label giving information about the version - if (bugReportRestClient.version && bugReportRestClient.build) - { - NSString *build = bugReportRestClient.build; - NSString *versionLabel = bugReportRestClient.version; - - // If this is not the app store version, be more accurate on the build origin - if ([build isEqualToString:[VectorL10n settingsConfigNoBuildInfo]]) - { - // This is a debug session from Xcode - versionLabel = [versionLabel stringByAppendingString:@"-debug"]; - } - else if (build && ![build containsString:@"master"]) - { - // This is a Jenkins build. Add the branch and the build number - NSString *buildString = [build stringByReplacingOccurrencesOfString:@" " withString:@"-"]; - versionLabel = [[versionLabel stringByAppendingString:@"-"] stringByAppendingString:buildString]; - } - - [gitHubLabels addObject:versionLabel]; - } - + NSMutableString *bugReportDescription = [NSMutableString stringWithString:_bugReportDescriptionTextView.text]; - - if (_reportCrash) - { - // Append the crash dump to the user description in order to ease triaging of GH issues - NSString *crashLogFile = [MXLogger crashLog]; - NSString *crashLog = [NSString stringWithContentsOfFile:crashLogFile encoding:NSUTF8StringEncoding error:nil]; - [bugReportDescription appendFormat:@"\n\n\n--------------------------------------------------------------------------------\n\n%@", crashLog]; - } - + // starting a background task to have a bit of extra time in case of user forgets about the report and sends the app to background __block UIBackgroundTaskIdentifier operationBackgroundId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [[UIApplication sharedApplication] endBackgroundTask:operationBackgroundId]; operationBackgroundId = UIBackgroundTaskInvalid; }]; - // Submit - [bugReportRestClient sendBugReport:bugReportDescription sendLogs:_sendLogs sendCrashLog:_reportCrash sendFiles:files attachGitHubLabels:gitHubLabels progress:^(MXBugReportState state, NSProgress *progress) { - + [bugReportRestClient vc_sendBugReportWithDescription:bugReportDescription + sendLogs:_sendLogs + sendCrashLog:_reportCrash + sendFiles:files + additionalLabels:nil + customFields:nil + progress:^(MXBugReportState state, NSProgress *progress) { + switch (state) { case MXBugReportStateProgressZipping: @@ -413,7 +341,7 @@ - (IBAction)onSendButtonPress:(id)sender self.sendingProgress.progress = progress.fractionCompleted; - } success:^{ + } success:^(NSString *reportUrl){ self->bugReportRestClient = nil; diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 0d72d28032..4cff1dac32 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -160,6 +160,7 @@ 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_AUTO_REPORT_DECRYPTION_ERRORS, LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX }; @@ -573,6 +574,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 addRowWithTag:LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX]; sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) @@ -1486,6 +1488,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 @@ -2458,6 +2475,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 (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX) { MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; @@ -3903,6 +3924,12 @@ - (void)toggleEnableRoomMessageBubbles:(UISwitch *)sender [roomDataSourceManager reset]; } + +- (void)toggleEnableAutoReportDecryptionErrors:(UISwitch *)sender +{ + RiotSettings.shared.enableUISIAutoReporting = sender.isOn; +} + #pragma mark - TextField listener - (IBAction)textFieldDidChange:(id)sender