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