Merge pull request #5743 from vector-im/langleyd/5017_uisi_autoreporter

App: UISI AutoReporting
This commit is contained in:
David Langley 2022-03-24 14:58:23 +00:00 committed by GitHub
commit 0f89eb014e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 658 additions and 107 deletions

View file

@ -191,6 +191,7 @@ final class BuildSettings: NSObject {
static let bugReportEndpointUrlString = "https://riot.im/bugreports" static let bugReportEndpointUrlString = "https://riot.im/bugreports"
// Use the name allocated by the bug report server // Use the name allocated by the bug report server
static let bugReportApplicationId = "riot-ios" static let bugReportApplicationId = "riot-ios"
static let bugReportUISIId = "element-auto-uisi"
// MARK: - Integrations // MARK: - Integrations
@ -379,6 +380,9 @@ final class BuildSettings: NSObject {
// MARK: - Secrets Recovery // MARK: - Secrets Recovery
static let secretsRecoveryAllowReset = true static let secretsRecoveryAllowReset = true
// MARK: - UISI Autoreporting
static let cryptoUISIAutoReportingEnabled = false
// MARK: - Polls // MARK: - Polls
static var pollsEnabled: Bool { static var pollsEnabled: Bool {

View file

@ -634,6 +634,7 @@ Tap the + to start adding people.";
"settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls";
"settings_labs_enabled_polls" = "Polls"; "settings_labs_enabled_polls" = "Polls";
"settings_labs_enable_threads" = "Threaded messaging"; "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_labs_use_only_latest_user_avatar_and_name" = "Show latest avatar and name for users in message history";
"settings_version" = "Version %@"; "settings_version" = "Version %@";
@ -2435,7 +2436,7 @@ Tap the + to start adding people.";
"language_picker_title" = "Choose a language"; "language_picker_title" = "Choose a language";
"language_picker_default_language" = "Default (%@)"; "language_picker_default_language" = "Default (%@)";
/* -*- /* -*-
Automatic localization for en Automatic localization for en
The following key/value pairs were extracted from the android i18n file: The following key/value pairs were extracted from the android i18n file:
@ -2518,17 +2519,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" = "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."; "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 Automatic localization for en
The following key/value pairs were extracted from the android i18n file: The following key/value pairs were extracted from the android i18n file:
@ -2536,9 +2537,9 @@ Tap the + to start adding people.";
*/ */
// titles // titles
// button names // button names
"send" = "Send"; "send" = "Send";
"copy_button_name" = "Copy"; "copy_button_name" = "Copy";
"resend" = "Resend"; "resend" = "Resend";
@ -2546,7 +2547,7 @@ Tap the + to start adding people.";
"share" = "Share"; "share" = "Share";
"delete" = "Delete"; "delete" = "Delete";
// actions // actions
"action_logout" = "Logout"; "action_logout" = "Logout";
"create_room" = "Create Room"; "create_room" = "Create Room";
"login" = "Login"; "login" = "Login";
@ -2561,32 +2562,32 @@ Tap the + to start adding people.";
"unban" = "Un-ban"; "unban" = "Un-ban";
"message_unsaved_changes" = "There are unsaved changes. Leaving will discard them."; "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_already_logged_in" = "Already logged in";
"login_error_must_start_http" = "URL must start with http[s]://"; "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 info dialog Screen
// room details 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."; "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_config" = "Configuration";
"settings_title_notifications" = "Notifications"; "settings_title_notifications" = "Notifications";
// Notification settings screen // Notification settings screen
"notification_settings_disable_all" = "Disable all notifications"; "notification_settings_disable_all" = "Disable all notifications";
"notification_settings_enable_notifications" = "Enable notifications"; "notification_settings_enable_notifications" = "Enable notifications";
"notification_settings_enable_notifications_warning" = "All notifications are currently disabled for all devices."; "notification_settings_enable_notifications_warning" = "All notifications are currently disabled for all devices.";
@ -2613,10 +2614,10 @@ Tap the + to start adding people.";
"notification_settings_by_default" = "By default..."; "notification_settings_by_default" = "By default...";
"notification_settings_notify_all_other" = "Notify for all other messages/rooms"; "notification_settings_notify_all_other" = "Notify for all other messages/rooms";
// gcm section // gcm section
"settings_config_identity_server" = "Identity server: %@"; "settings_config_identity_server" = "Identity server: %@";
// Settings keys // Settings keys
// call string // call string
"call_connecting" = "Connecting…"; "call_connecting" = "Connecting…";
@ -2649,4 +2650,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_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_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."; "ssl_only_accept" = "ONLY accept the certificate if the server administrator has published a fingerprint that matches the one above.";

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<Publishers.Delay<Just<Publishers.Buffer<Self>.Output>, DispatchQueue>, Publishers.Buffer<Self>.Failure>,
Publishers.Buffer<Self>
> {
return buffer(size: .max, prefetch: .byRequest, whenFull: .dropNewest)
.flatMap(maxPublishers: .max(1)) {
Just($0).delay(for: .seconds(spacingDelay), scheduler: scheduler)
}
}
}

View file

@ -6699,6 +6699,10 @@ public class VectorL10n: NSObject {
public static var settingsLabsE2eEncryptionPromptMessage: String { public static var settingsLabsE2eEncryptionPromptMessage: String {
return VectorL10n.tr("Vector", "settings_labs_e2e_encryption_prompt_message") 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 /// Ring for group calls
public static var settingsLabsEnableRingingForGroupCalls: String { public static var settingsLabsEnableRingingForGroupCalls: String {
return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls")

View file

@ -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<Notification, Never> {
return NotificationCenter.default.publisher(for: .userDefaultValueUpdated)
.filter({ $0.object as? String == key })
.eraseToAnyPublisher()
}
}

View file

@ -30,6 +30,7 @@ final class RiotSettings: NSObject {
static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif" static let pinRoomsWithMissedNotificationsOnHome = "pinRoomsWithMissedNotif"
static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread" static let pinRoomsWithUnreadMessagesOnHome = "pinRoomsWithUnread"
static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace" static let showAllRoomsInHomeSpace = "showAllRoomsInHomeSpace"
static let enableUISIAutoReporting = "enableUISIAutoReporting"
} }
static let shared = RiotSettings() static let shared = RiotSettings()
@ -146,6 +147,10 @@ final class RiotSettings: NSObject {
@UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults)
var enableThreads var enableThreads
/// Indicates if auto reporting of decryption errors is enabled
@UserDefault(key: UserDefaultsKeys.enableUISIAutoReporting, defaultValue: BuildSettings.cryptoUISIAutoReportingEnabled, storage: defaults)
var enableUISIAutoReporting
// MARK: Calls // MARK: Calls
/// Indicate if `allowStunServerFallback` settings has been set once. /// Indicate if `allowStunServerFallback` settings has been set once.

View file

@ -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<ReportInfo>()
private let e2eDetectedSubject = PassthroughSubject<UISIDetectedMessage, Never>()
private let matchingRSRequestSubject = PassthroughSubject<MXEvent, Never>()
private var cancellables = Set<AnyCancellable>()
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<NSDictionary>()
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 ?? ""
]
)
}
}

View file

@ -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)"
}
}

View file

@ -225,6 +225,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
@property (nonatomic, strong) PushNotificationStore *pushNotificationStore; @property (nonatomic, strong) PushNotificationStore *pushNotificationStore;
@property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService; @property (nonatomic, strong) LocalAuthenticationService *localAuthenticationService;
@property (nonatomic, strong, readwrite) CallPresenter *callPresenter; @property (nonatomic, strong, readwrite) CallPresenter *callPresenter;
@property (nonatomic, strong, readwrite) id uisiAutoReporter;
@property (nonatomic, strong) MajorUpdateManager *majorUpdateManager; @property (nonatomic, strong) MajorUpdateManager *majorUpdateManager;
@ -471,6 +472,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
self.spaceFeatureUnavailablePresenter = [SpaceFeatureUnavailablePresenter new]; self.spaceFeatureUnavailablePresenter = [SpaceFeatureUnavailablePresenter new];
if (@available(iOS 14.0, *)) {
self.uisiAutoReporter = [[UISIAutoReporter alloc] init];
}
// Add matrix observers, and initialize matrix sessions if the app is not launched in background. // Add matrix observers, and initialize matrix sessions if the app is not launched in background.
[self initMatrixSessions]; [self initMatrixSessions];
@ -2152,6 +2157,17 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// register the session to the call service // register the session to the call service
[_callPresenter addMatrixSession:mxSession]; [_callPresenter addMatrixSession:mxSession];
// register the session to the uisi auto-reporter
if (_uisiAutoReporter != nil)
{
if (@available(iOS 14.0, *))
{
UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter;
[uisiAutoReporter add:mxSession];
}
}
[_callPresenter addMatrixSession:mxSession];
[mxSessionArray addObject:mxSession]; [mxSessionArray addObject:mxSession];
// Do the one time check on device id // Do the one time check on device id
@ -2167,6 +2183,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
// remove session from the call service // remove session from the call service
[_callPresenter removeMatrixSession:mxSession]; [_callPresenter removeMatrixSession:mxSession];
// register the session to the uisi auto-reporter
if (_uisiAutoReporter != nil)
{
if (@available(iOS 14.0, *))
{
UISIAutoReporter* uisiAutoReporter = (UISIAutoReporter*)_uisiAutoReporter;
[uisiAutoReporter remove:mxSession];
}
}
// Update the widgets manager // Update the widgets manager
[[WidgetManager sharedManager] removeMatrixSession:mxSession]; [[WidgetManager sharedManager] removeMatrixSession:mxSession];

View file

@ -295,47 +295,8 @@
{ {
self.isSendingLogs = YES; self.isSendingLogs = YES;
// Setup data to send bugReportRestClient = [MXBugReportRestClient vc_bugReportRestClientWithAppName:BuildSettings.bugReportApplicationId];
bugReportRestClient = [[MXBugReportRestClient alloc] initWithBugReportEndpoint:BuildSettings.bugReportEndpointUrlString];
// App info
bugReportRestClient.appName = BuildSettings.bugReportApplicationId;
bugReportRestClient.version = [AppDelegate theDelegate].appVersion;
bugReportRestClient.build = [AppDelegate theDelegate].build;
// Device info
bugReportRestClient.deviceModel = [GBDeviceInfo deviceInfo].modelString;
bugReportRestClient.deviceOS = [NSString stringWithFormat:@"%@ %@", [[UIDevice currentDevice] systemName], [[UIDevice currentDevice] systemVersion]];
// User info (TODO: handle multi-account and find a way to expose them in rageshake API)
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
MXKAccount *mainAccount = [MXKAccountManager sharedManager].accounts.firstObject;
if (mainAccount.mxSession.myUser.userId)
{
userInfo[@"user_id"] = mainAccount.mxSession.myUser.userId;
}
if (mainAccount.mxSession.matrixRestClient.credentials.deviceId)
{
userInfo[@"device_id"] = mainAccount.mxSession.matrixRestClient.credentials.deviceId;
}
userInfo[@"locale"] = [NSLocale preferredLanguages][0];
userInfo[@"default_app_language"] = [[NSBundle mainBundle] preferredLocalizations][0]; // The language chosen by the OS
userInfo[@"app_language"] = [NSBundle mxk_language] ? [NSBundle mxk_language] : userInfo[@"default_app_language"]; // The language chosen by the user
// Application settings
userInfo[@"lazy_loading"] = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers ? @"ON" : @"OFF";
NSDate *currentDate = [NSDate date];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
userInfo[@"local_time"] = [dateFormatter stringFromDate:currentDate];
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
userInfo[@"utc_time"] = [dateFormatter stringFromDate:currentDate];
bugReportRestClient.others = userInfo;
// Screenshot // Screenshot
NSArray<NSURL*> *files; NSArray<NSURL*> *files;
if (_screenshot && _sendScreenshot) if (_screenshot && _sendScreenshot)
@ -347,56 +308,23 @@
files = @[screenShotFile]; files = @[screenShotFile];
} }
// Prepare labels to attach to the GitHub issue
NSMutableArray<NSString*> *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]; 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 // 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:^{ __block UIBackgroundTaskIdentifier operationBackgroundId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:operationBackgroundId]; [[UIApplication sharedApplication] endBackgroundTask:operationBackgroundId];
operationBackgroundId = UIBackgroundTaskInvalid; operationBackgroundId = UIBackgroundTaskInvalid;
}]; }];
// Submit [bugReportRestClient vc_sendBugReportWithDescription:bugReportDescription
[bugReportRestClient sendBugReport:bugReportDescription sendLogs:_sendLogs sendCrashLog:_reportCrash sendFiles:files attachGitHubLabels:gitHubLabels progress:^(MXBugReportState state, NSProgress *progress) { sendLogs:_sendLogs
sendCrashLog:_reportCrash
sendFiles:files
additionalLabels:nil
customFields:nil
progress:^(MXBugReportState state, NSProgress *progress) {
switch (state) switch (state)
{ {
case MXBugReportStateProgressZipping: case MXBugReportStateProgressZipping:
@ -413,7 +341,7 @@
self.sendingProgress.progress = progress.fractionCompleted; self.sendingProgress.progress = progress.fractionCompleted;
} success:^{ } success:^(NSString *reportUrl){
self->bugReportRestClient = nil; self->bugReportRestClient = nil;

View file

@ -160,6 +160,7 @@ typedef NS_ENUM(NSUInteger, LABS_ENABLE)
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
LABS_ENABLE_THREADS_INDEX, LABS_ENABLE_THREADS_INDEX,
LABS_ENABLE_MESSAGE_BUBBLES_INDEX, LABS_ENABLE_MESSAGE_BUBBLES_INDEX,
LABS_ENABLE_AUTO_REPORT_DECRYPTION_ERRORS,
LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX
}; };
@ -573,6 +574,7 @@ TableViewSectionsDelegate>
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
[sectionLabs addRowWithTag:LABS_ENABLE_MESSAGE_BUBBLES_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 addRowWithTag:LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX];
sectionLabs.headerTitle = [VectorL10n settingsLabs]; sectionLabs.headerTitle = [VectorL10n settingsLabs];
if (sectionLabs.hasAnyRows) if (sectionLabs.hasAnyRows)
@ -1486,6 +1488,21 @@ TableViewSectionsDelegate>
return labelAndSwitchCell; 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 #pragma mark - 3Pid Add
- (void)showAuthenticationIfNeededForAdding:(MX3PIDMedium)medium withSession:(MXSession*)session completion:(void (^)(NSDictionary* authParams))completion - (void)showAuthenticationIfNeededForAdding:(MX3PIDMedium)medium withSession:(MXSession*)session completion:(void (^)(NSDictionary* authParams))completion
@ -2458,6 +2475,10 @@ TableViewSectionsDelegate>
{ {
cell = [self buildMessageBubblesCellForTableView:tableView atIndexPath:indexPath]; 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) else if (row == LABS_USE_ONLY_LATEST_USER_AVATAR_AND_NAME_INDEX)
{ {
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
@ -3903,6 +3924,12 @@ TableViewSectionsDelegate>
[roomDataSourceManager reset]; [roomDataSourceManager reset];
} }
- (void)toggleEnableAutoReportDecryptionErrors:(UISwitch *)sender
{
RiotSettings.shared.enableUISIAutoReporting = sender.isOn;
}
#pragma mark - TextField listener #pragma mark - TextField listener
- (IBAction)textFieldDidChange:(id)sender - (IBAction)textFieldDidChange:(id)sender