From e266f7f0cbc651cd61ea2802c983fc595e7dfd70 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Apr 2024 13:01:19 +0200 Subject: [PATCH] Analytics | Report expected UTDs as HistoricalMessage error --- .../DecryptionFailure+Analytics.swift | 21 +- .../Modules/Analytics/DecryptionFailure.swift | 5 + .../Analytics/DecryptionFailureTracker.swift | 18 +- Riot/Utils/EventFormatter.m | 2 +- RiotTests/DecryptionFailureTrackerTests.swift | 210 ++++++++++++++- RiotTests/FakeUtils.swift | 242 +++++++++++++++++- 6 files changed, 477 insertions(+), 21 deletions(-) diff --git a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift index cd129aee35..5ed3c36c2c 100644 --- a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift +++ b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift @@ -21,23 +21,34 @@ extension DecryptionFailure { public func toAnalyticsEvent() -> AnalyticsEvent.Error { - let timeToDecryptMillis: Int = if self.timeToDecrypt != nil { - Int(self.timeToDecrypt! * 1000) + let timeToDecryptMillis: Int = if let ttd = self.timeToDecrypt { + Int(ttd * 1000) } else { -1 } + + let isHistoricalEvent = if let localAge = self.eventLocalAgeMillis { + localAge < 0 + } else { false } + + let errorName = if isHistoricalEvent && self.trustOwnIdentityAtTimeOfFailure == false { + AnalyticsEvent.Error.Name.HistoricalMessage + } else { + self.reason.errorName + } + return AnalyticsEvent.Error( context: self.context, cryptoModule: .Rust, cryptoSDK: .Rust, domain: .E2EE, - eventLocalAgeMillis: nil, + eventLocalAgeMillis: self.eventLocalAgeMillis, isFederated: nil, isMatrixDotOrg: nil, - name: self.reason.errorName, + name: errorName, timeToDecryptMillis: timeToDecryptMillis, - userTrustsOwnIdentity: nil, + userTrustsOwnIdentity: self.trustOwnIdentityAtTimeOfFailure, wasVisibleToUser: nil ) } diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 9e0ca57858..6d3f6c1a83 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -47,6 +47,11 @@ import AnalyticsEvents /// UTDs can be permanent or temporary. If temporary, this field will contain the time it took to decrypt the message in milliseconds. If permanent should be nil var timeToDecrypt: TimeInterval? + /// Was the current cross-signing identity trusted at the time of decryption + var trustOwnIdentityAtTimeOfFailure: Bool? + + var eventLocalAgeMillis: Int? + init(failedEventId: String, reason: DecryptionFailureReason, context: String, ts: TimeInterval) { self.failedEventId = failedEventId self.reason = reason diff --git a/Riot/Modules/Analytics/DecryptionFailureTracker.swift b/Riot/Modules/Analytics/DecryptionFailureTracker.swift index 19b8afb19c..503c733ba3 100644 --- a/Riot/Modules/Analytics/DecryptionFailureTracker.swift +++ b/Riot/Modules/Analytics/DecryptionFailureTracker.swift @@ -62,14 +62,14 @@ class DecryptionFailureTracker: NSObject { selector: #selector(eventDidDecrypt(_:)), name: .mxEventDidDecrypt, object: nil) - } @objc - func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, myUser userId: String) { + func reportUnableToDecryptError(forEvent event: MXEvent, withRoomState roomState: MXRoomState, mySession: MXSession) { if reportedFailures[event.eventId] != nil || trackedEvents.contains(event.eventId) { return } + guard let userId = mySession.myUserId else { return } // Filter out "expected" UTDs // We cannot decrypt messages sent before the user joined the room @@ -82,6 +82,12 @@ class DecryptionFailureTracker: NSObject { guard let error = event.decryptionError as? NSError else { return } + let eventOrigin = event.originServerTs + let deviceTimestamp = mySession.crypto.deviceCreationTs + // If negative it's an historical event relative to the current session + let eventRelativeAgeMillis = Int(eventOrigin) - Int(deviceTimestamp) + let isSessionVerified = mySession.crypto.crossSigning.canTrustCrossSigning + var reason = DecryptionFailureReason.unspecified if error.code == MXDecryptingErrorUnknownInboundSessionIdCode.rawValue { @@ -92,7 +98,13 @@ class DecryptionFailureTracker: NSObject { let context = String(format: "code: %ld, description: %@", error.code, event.decryptionError.localizedDescription) - reportedFailures[failedEventId] = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs()) + let failure = DecryptionFailure(failedEventId: failedEventId, reason: reason, context: context, ts: self.timeProvider.nowTs()) + + failure.eventLocalAgeMillis = Int(exactly: eventRelativeAgeMillis) + failure.trustOwnIdentityAtTimeOfFailure = isSessionVerified + + reportedFailures[failedEventId] = failure + // Start the ticker if needed. There is no need to have a ticker if no failures are tracked if checkFailuresTimer == nil { diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index 4494bdbb2d..d53a3e19a0 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -334,7 +334,7 @@ - (NSAttributedString *)unsafeAttributedStringFromEvent:(MXEvent *)event { // Track e2e failures dispatch_async(dispatch_get_main_queue(), ^{ - [[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState myUser:self->mxSession.myUser.userId]; + [[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState mySession:self->mxSession]; }); if (event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode) diff --git a/RiotTests/DecryptionFailureTrackerTests.swift b/RiotTests/DecryptionFailureTrackerTests.swift index 7cd9bf480f..c142514cb0 100644 --- a/RiotTests/DecryptionFailureTrackerTests.swift +++ b/RiotTests/DecryptionFailureTrackerTests.swift @@ -41,10 +41,23 @@ class DecryptionFailureTrackerTests: XCTestCase { } let timeShifter = TimeShifter() + var fakeCrypto: FakeCrypto! + var fakeSession: FakeSession! + var fakeCrossSigning: FakeCrossSigning! + + override func setUp() { + super.setUp() + self.fakeCrypto = FakeCrypto() + self.fakeCrossSigning = FakeCrossSigning() + self.fakeCrypto.crossSigning = self.fakeCrossSigning + self.fakeSession = FakeSession(mockCrypto: self.fakeCrypto) + } + func test_grace_period() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -61,7 +74,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); timeShifter.timestamp = TimeInterval(2) @@ -83,6 +96,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_report_ratcheted_key_utd() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -99,7 +113,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Pass the max period timeShifter.timestamp = TimeInterval(70) @@ -112,6 +126,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_report_unspecified_error() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -128,7 +143,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Pass the max period timeShifter.timestamp = TimeInterval(70) @@ -143,6 +158,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_do_not_double_report() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -160,7 +176,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Pass the max period timeShifter.timestamp = TimeInterval(70) @@ -171,7 +187,7 @@ class DecryptionFailureTrackerTests: XCTestCase { // Try to report again the same event testDelegate.reportedFailure = nil - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Pass the grace period timeShifter.timestamp = TimeInterval(10) @@ -184,6 +200,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_ignore_not_member() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -203,7 +220,7 @@ class DecryptionFailureTrackerTests: XCTestCase { fakeMembers.mockMembers[myUser] = MXMembership.ban fakeRoomState.mockMembers = fakeMembers - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Pass the grace period timeShifter.timestamp = TimeInterval(5) @@ -218,6 +235,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_notification_center() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -235,7 +253,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Shift time below GRACE_PERIOD timeShifter.timestamp = TimeInterval(2) @@ -258,6 +276,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_should_report_late_decrypt() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -275,7 +294,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Simulate succesful decryption after grace period but before max wait timeShifter.timestamp = TimeInterval(20) @@ -302,6 +321,7 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_should_report_permanent_decryption_error() { let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; let decryptionFailureTracker = DecryptionFailureTracker(); decryptionFailureTracker.timeProvider = timeShifter; @@ -319,7 +339,7 @@ class DecryptionFailureTrackerTests: XCTestCase { let fakeRoomState = FakeRoomState(); fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) - decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, myUser: myUser); + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); // Simulate succesful decryption after max wait timeShifter.timestamp = TimeInterval(70) @@ -337,5 +357,177 @@ class DecryptionFailureTrackerTests: XCTestCase { XCTAssertEqual(analyticsError.timeToDecryptMillis, -1) } + + + func test_should_report_trust_status_at_decryption_time() { + + let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = TimeInterval(0) + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + // set session as not yet verified + fakeCrossSigning.canTrustCrossSigning = false + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); + + // set verified now + fakeCrossSigning.canTrustCrossSigning = true + + // Simulate succesful decryption after max wait + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.trustOwnIdentityAtTimeOfFailure, false); + + // Assert that it's converted to -1 for reporting + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.userTrustsOwnIdentity, false) + + // Report a new error now that session is verified + + let fakeEvent2 = FakeEvent(id: "$0001"); + fakeEvent2.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent2, withRoomState: fakeRoomState, mySession: fakeSession); + + // Simulate permanent UTD + timeShifter.timestamp = TimeInterval(140) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.failedEventId, "$0001"); + XCTAssertEqual(testDelegate.reportedFailure?.trustOwnIdentityAtTimeOfFailure, true); + + let analyticsError2 = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError2.userTrustsOwnIdentity, true) + + } + + + func test_should_report_event_age() { + + let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; + + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + + let sessionCreationTimeMillis = format.date(from: "2024-03-09T10:00:00Z")!.timeIntervalSince1970 * 1000 + + let now = format.date(from: "2024-03-09T10:02:00Z")!.timeIntervalSince1970 + + // 5mn after session was created + let postCreationMessageTs = UInt64(format.date(from: "2024-03-09T10:05:00Z")!.timeIntervalSince1970 * 1000) + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = now + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.mockOrigineServerTs = postCreationMessageTs; + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + fakeCrypto.deviceCreationTs = UInt64(sessionCreationTimeMillis) + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); + + // set verified now + fakeCrossSigning.canTrustCrossSigning = true + + // Simulate permanent UTD + timeShifter.timestamp = now + TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.eventLocalAgeMillis, 5 * 60 * 1000); + + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.eventLocalAgeMillis, 5 * 60 * 1000) + + } + + + func test_should_report_expected_utds() { + + let myUser = "test@example.com"; + fakeSession.mockUserId = myUser; + + let format = DateFormatter() + format.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + + let sessionCreationTimeMillis = format.date(from: "2024-03-09T10:00:00Z")!.timeIntervalSince1970 * 1000 + + let now = format.date(from: "2024-03-09T10:02:00Z")!.timeIntervalSince1970 + + // 1 day before session was created + let historicalMessageTs = UInt64(format.date(from: "2024-03-08T10:00:00Z")!.timeIntervalSince1970 * 1000) + + let decryptionFailureTracker = DecryptionFailureTracker(); + decryptionFailureTracker.timeProvider = timeShifter; + + let testDelegate = AnalyticsDelegate(); + + decryptionFailureTracker.delegate = testDelegate; + + timeShifter.timestamp = now + + let fakeEvent = FakeEvent(id: "$0000"); + fakeEvent.mockOrigineServerTs = historicalMessageTs; + fakeEvent.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + fakeCrypto.deviceCreationTs = UInt64(sessionCreationTimeMillis) + + let fakeRoomState = FakeRoomState(); + fakeRoomState.mockMembers = FakeRoomMembers(joined: [myUser]) + + fakeCrossSigning.canTrustCrossSigning = false + + decryptionFailureTracker.reportUnableToDecryptError(forEvent: fakeEvent, withRoomState: fakeRoomState, mySession: fakeSession); + + // set verified now + fakeCrossSigning.canTrustCrossSigning = true + + // Simulate permanent UTD + timeShifter.timestamp = now + TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + // Event should have been reported as a late decrypt + XCTAssertEqual(testDelegate.reportedFailure?.eventLocalAgeMillis, -24 * 60 * 60 * 1000); + + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.name, .HistoricalMessage) + + } + } diff --git a/RiotTests/FakeUtils.swift b/RiotTests/FakeUtils.swift index 7bd350e4bc..f6b1fe4771 100644 --- a/RiotTests/FakeUtils.swift +++ b/RiotTests/FakeUtils.swift @@ -1,4 +1,4 @@ -// +// // Copyright 2024 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -22,6 +22,7 @@ class FakeEvent: MXEvent { var mockEventId: String; var mockSender: String!; var mockDecryptionError: Error? + var mockOrigineServerTs: UInt64 = 0 init(id: String) { mockEventId = id @@ -31,7 +32,7 @@ class FakeEvent: MXEvent { required init?(coder: NSCoder) { fatalError() } - + override var sender: String! { get { return mockSender } set { mockSender = newValue } @@ -47,6 +48,12 @@ class FakeEvent: MXEvent { set { mockDecryptionError = newValue } } + override var originServerTs: UInt64 { + get { return mockOrigineServerTs } + set { mockOrigineServerTs = newValue } + + } + } @@ -80,7 +87,7 @@ class FakeRoomMember: MXRoomMember { get { return mockUserId } set { mockUserId = newValue } } - + } @@ -107,3 +114,232 @@ class FakeRoomMembers: MXRoomMembers { } } + +class FakeSession: MXSession { + + var mockCrypto: MXCrypto? = FakeCrypto() + + var mockUserId: String = "@alice:localhost" + + init(mockCrypto: MXCrypto? = nil) { + self.mockCrypto = mockCrypto + super.init() + } + + override var crypto: MXCrypto? { + get { + return mockCrypto + } + set { + // nothing + } + } + + override var myUserId: String! { + get { + return mockUserId + } + set { + mockUserId = newValue + } + } +} + +class FakeCrypto: NSObject, MXCrypto { + + + var version: String = "" + + var deviceCurve25519Key: String? + + var deviceEd25519Key: String? + + var deviceCreationTs: UInt64 = 0 + + var backup: MXKeyBackup? + + var keyVerificationManager: MXKeyVerificationManager = FakeKeyVerificationManager() + + var crossSigning: MXCrossSigning = FakeCrossSigning() + + var recoveryService: MXRecoveryService! = nil + + var dehydrationService: MatrixSDK.DehydrationService! = nil + + override init() { + super.init() + } + + func start(_ onComplete: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func close(_ deleteStore: Bool) { + + } + + func isRoomEncrypted(_ roomId: String) -> Bool { + return true + } + + func encryptEventContent(_ eventContent: [AnyHashable : Any], withType eventType: String, in room: MXRoom, success: (([AnyHashable : Any], String) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? { + return nil + } + + func decryptEvents(_ events: [MXEvent], inTimeline timeline: String?, onComplete: (([MXEventDecryptionResult]) -> Void)? = nil) { + + } + + func ensureEncryption(inRoom roomId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? { + return nil + } + + func eventDeviceInfo(_ event: MXEvent) -> MXDeviceInfo? { + return nil + } + + func discardOutboundGroupSessionForRoom(withRoomId roomId: String, onComplete: (() -> Void)? = nil) { + + } + + func handle(_ syncResponse: MXSyncResponse, onComplete: @escaping () -> Void) { + + } + + func setDeviceVerification(_ verificationStatus: MXDeviceVerification, forDevice deviceId: String, ofUser userId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func setUserVerification(_ verificationStatus: Bool, forUser userId: String, success: (() -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func trustLevel(forUser userId: String) -> MXUserTrustLevel { + return MXUserTrustLevel.init(crossSigningVerified: true, locallyVerified: true) + } + + func deviceTrustLevel(forDevice deviceId: String, ofUser userId: String) -> MXDeviceTrustLevel? { + return nil + } + + func trustLevelSummary(forUserIds userIds: [String], forceDownload: Bool, success: ((MXUsersTrustLevelSummary?) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func downloadKeys(_ userIds: [String], forceDownload: Bool, success: ((MXUsersDevicesMap?, [String : MXCrossSigningInfo]?) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) -> MXHTTPOperation? { + return nil + } + + func devices(forUser userId: String) -> [String : MXDeviceInfo] { + return [:]; + } + + func device(withDeviceId deviceId: String, ofUser userId: String) -> MXDeviceInfo? { + return nil + } + + func exportRoomKeys(withPassword password: String, success: ((Data) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func importRoomKeys(_ keyFile: Data, withPassword password: String, success: ((UInt, UInt) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func reRequestRoomKey(for event: MXEvent) { + + } + + var globalBlacklistUnverifiedDevices: Bool = false + + func isBlacklistUnverifiedDevices(inRoom roomId: String) -> Bool { + return false + } + + func setBlacklistUnverifiedDevicesInRoom(_ roomId: String, blacklist: Bool) { + + } + +} + + +class FakeCrossSigning: NSObject, MXCrossSigning { + + + override init() { + super.init() + } + + var state: MXCrossSigningState = MXCrossSigningState.trustCrossSigning + + var myUserCrossSigningKeys: MXCrossSigningInfo? = nil + + var canTrustCrossSigning: Bool = true + + var canCrossSign: Bool = true + + var hasAllPrivateKeys: Bool = true + + func refreshState(success: ((Bool) -> Void)?, failure: ((Swift.Error) -> Void)? = nil) { + + } + + func setup(withPassword password: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func setup(withAuthParams authParams: [AnyHashable : Any], success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func crossSignDevice(withDeviceId deviceId: String, userId: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func signUser(withUserId userId: String, success: @escaping () -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func crossSigningKeys(forUser userId: String) -> MXCrossSigningInfo? { + return nil + } + +} + +class FakeKeyVerificationManager: NSObject, MXKeyVerificationManager { + + override init() { + super.init() + } + + func requestVerificationByToDevice(withUserId userId: String, deviceIds: [String]?, methods: [String], success: @escaping (MXKeyVerificationRequest) -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func requestVerificationByDM(withUserId userId: String, roomId: String?, fallbackText: String, methods: [String], success: @escaping (MXKeyVerificationRequest) -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + var pendingRequests: [MXKeyVerificationRequest] = [] + + func beginKeyVerification(from request: MXKeyVerificationRequest, method: String, success: @escaping (MXKeyVerificationTransaction) -> Void, failure: @escaping (Swift.Error) -> Void) { + + } + + func transactions(_ complete: @escaping ([MXKeyVerificationTransaction]) -> Void) { + + } + + func keyVerification(fromKeyVerificationEvent event: MXEvent, roomId: String, success: @escaping (MXKeyVerification) -> Void, failure: @escaping (Swift.Error) -> Void) -> MXHTTPOperation? { + return nil + } + + func qrCodeTransaction(withTransactionId transactionId: String) -> MXQRCodeTransaction? { + return nil + } + + func removeQRCodeTransaction(withTransactionId transactionId: String) { + + } + +}