diff --git a/CHANGES.md b/CHANGES.md index eb5267b66..0e6621096 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,14 @@ +## Changes in 1.11.10 (2024-05-01) + +🙌 Improvements + +- Upgrade MatrixSDK version ([v0.27.7](https://github.com/matrix-org/matrix-ios-sdk/releases/tag/v0.27.7)). + +Others + +- Improvements to reporting of decryption failures. + + ## Changes in 1.11.9 (2024-04-02) Others diff --git a/Config/AppVersion.xcconfig b/Config/AppVersion.xcconfig index 4cf4cbfbe..5df7a12db 100644 --- a/Config/AppVersion.xcconfig +++ b/Config/AppVersion.xcconfig @@ -15,5 +15,5 @@ // // Version -MARKETING_VERSION = 1.11.9 -CURRENT_PROJECT_VERSION = 1.11.9 +MARKETING_VERSION = 1.11.10 +CURRENT_PROJECT_VERSION = 1.11.10 diff --git a/Podfile b/Podfile index 9ad4fefa2..f2df4910e 100644 --- a/Podfile +++ b/Podfile @@ -16,7 +16,7 @@ use_frameworks! # - `{ :specHash => {sdk spec hash}` to depend on specific pod options (:git => …, :podspec => …) for MatrixSDK repo. Used by Fastfile during CI # # Warning: our internal tooling depends on the name of this variable name, so be sure not to change it -$matrixSDKVersion = '= 0.27.6' +$matrixSDKVersion = '= 0.27.7' # $matrixSDKVersion = :local # $matrixSDKVersion = { :branch => 'develop'} # $matrixSDKVersion = { :specHash => { git: 'https://git.io/fork123', branch: 'fix' } } diff --git a/Podfile.lock b/Podfile.lock index 07f1fb4d9..c8298201a 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -39,9 +39,9 @@ PODS: - LoggerAPI (1.9.200): - Logging (~> 1.1) - Logging (1.4.0) - - MatrixSDK (0.27.6): - - MatrixSDK/Core (= 0.27.6) - - MatrixSDK/Core (0.27.6): + - MatrixSDK (0.27.7): + - MatrixSDK/Core (= 0.27.7) + - MatrixSDK/Core (0.27.7): - AFNetworking (~> 4.0.0) - GZIP (~> 1.3.0) - libbase58 (~> 0.1.4) @@ -49,7 +49,7 @@ PODS: - OLMKit (~> 3.2.5) - Realm (= 10.27.0) - SwiftyBeaver (= 1.9.5) - - MatrixSDK/JingleCallStack (0.27.6): + - MatrixSDK/JingleCallStack (0.27.7): - JitsiMeetSDKLite (= 8.1.2-lite) - MatrixSDK/Core - MatrixSDKCrypto (0.3.13) @@ -102,8 +102,8 @@ DEPENDENCIES: - KeychainAccess (~> 4.2.2) - KTCenterFlowLayout (~> 1.3.1) - libPhoneNumber-iOS (~> 0.9.13) - - MatrixSDK (= 0.27.6) - - MatrixSDK/JingleCallStack (= 0.27.6) + - MatrixSDK (= 0.27.7) + - MatrixSDK/JingleCallStack (= 0.27.7) - OLMKit - PostHog (~> 2.0.0) - ReadMoreTextView (~> 3.0.1) @@ -187,7 +187,7 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75 LoggerAPI: ad9c4a6f1e32f518fdb43a1347ac14d765ab5e3d Logging: beeb016c9c80cf77042d62e83495816847ef108b - MatrixSDK: 4129ab9c0acda1d0aad50b1c9765bd795b8d70b9 + MatrixSDK: e07b2309f3c6498c1df987441da7006d099c47a4 MatrixSDKCrypto: bf08b72f2cd015d8749420a2b8b92fc0536bedf4 OLMKit: da115f16582e47626616874e20f7bb92222c7a51 PostHog: 660ec6c9d80cec17b685e148f17f6785a88b597d @@ -208,6 +208,6 @@ SPEC CHECKSUMS: zxcvbn-ios: fef98b7c80f1512ff0eec47ac1fa399fc00f7e3c ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: c87b532985dd755b373732f841e3bcfe616f4e4f +PODFILE CHECKSUM: 1197abec9c5affbef652747dd5cd6aaf00ef3a47 COCOAPODS: 1.14.3 diff --git a/Riot/Assets/fa.lproj/Vector.strings b/Riot/Assets/fa.lproj/Vector.strings index a7f7b720c..4948e2e90 100644 --- a/Riot/Assets/fa.lproj/Vector.strings +++ b/Riot/Assets/fa.lproj/Vector.strings @@ -97,7 +97,7 @@ "contacts_address_book_matrix_users_toggle" = "فقط کاربران ماتریس"; "directory_search_results_more_than" = ">%tu نتیجه برای %@ یافت شد"; "directory_search_results" = "%tu نتیجه برای %@ یافت شد"; -"search_in_progress" = "در حال جست وجو"; +"search_in_progress" = "جوییدن…"; "search_no_result" = "نتیچه ای یافت نشد"; "search_people_placeholder" = "جستجو بر اساس شناسه کاربری، نام یا آدرس ایمیل"; "search_default_placeholder" = "جست و جو"; @@ -246,7 +246,7 @@ "room_accessibility_search" = "جستجو"; "room_message_edits_history_title" = "ویرایش های پیام"; "room_resource_usage_limit_reached_message_2" = "برخی از کاربران نمی توانند وارد سیستم شوند."; -"room_resource_limit_exceeded_message_contact_3" = " برای ادامه استفاده از این سرویس"; +"room_resource_limit_exceeded_message_contact_3" = " برای ادامه استفاده از این خدمت."; "room_resource_limit_exceeded_message_contact_2_link" = "با سرپرست سرویس خود تماس بگیرید"; "room_resource_limit_exceeded_message_contact_1" = " لطفا "; "room_predecessor_link" = "برای دیدن پیام‌های قدیمی‌تر به اینجاضربه بزنید."; @@ -416,7 +416,7 @@ "event_formatter_call_retry" = "تلاش مجدد"; "event_formatter_call_answer" = "پاسخ"; "event_formatter_call_decline" = "رد تماس"; -"event_formatter_call_back" = "تماس"; +"event_formatter_call_back" = "پاسخ تماس"; "event_formatter_call_connection_failed" = "ارتباط ناموفق بود"; "event_formatter_call_missed_video" = "تماس ویدیویی از دست رفته"; "event_formatter_call_missed_voice" = "تماس صوتی از دست رفته"; @@ -464,7 +464,7 @@ "group_participants_invited_section" = "دعوت شده"; "group_participants_invite_malformed_id_title" = "خطای دعوت"; "group_participants_invite_another_user" = "جستجو / دعوت با شناسه کاربری یا نام"; -"group_participants_filter_members" = "اعضای انجمن را فیلتر کنید"; +"group_participants_filter_members" = "پالایش اعضای اجتماع"; "group_participants_invite_prompt_msg" = "آیا مطمئنید که می خواهید %@ را به این گروه دعوت کنید؟"; "group_participants_invite_prompt_title" = "تایید"; "group_participants_remove_prompt_msg" = "آیا مطمئنید که می خواهید %@ را از این گروه حذف کنید؟"; @@ -570,7 +570,7 @@ "room_details_files" = "بارگذاری شده ها"; "room_details_people" = "اعضا"; "room_details_title_for_dm" = "جزییات"; -"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "به هر حال قطع کن"; +"identity_server_settings_alert_disconnect_still_sharing_3pid_button" = "قطع شدن به هر روی"; "identity_server_settings_alert_disconnect_button" = "قطع کردن"; "identity_server_settings_disconnect" = "قطع کردن"; "identity_server_settings_change" = "تغییر دادن"; @@ -652,7 +652,7 @@ "settings_confirm_password" = "تایید رمز"; "settings_new_password" = "رمز جدید"; "settings_old_password" = "رمز قدیمی"; -"settings_third_party_notices" = "اعلامیه های شخص ثالث"; +"settings_third_party_notices" = "یادآوری‌های سوم‌شخص"; "settings_privacy_policy" = "سیاست حفظ حریم خصوصی"; "settings_term_conditions" = "شرایط و ضوابط"; "settings_copyright" = "کپی رایت"; @@ -684,7 +684,7 @@ "settings_room_upgrades" = "ارتقاء اتاق"; "settings_messages_by_a_bot" = "پیام های ربات"; "settings_call_invitations" = "دعوت نامه های تماس"; -"settings_room_invitations" = "دعوت نامه های اتاق"; +"settings_room_invitations" = "دعوت‌های اتاق"; "settings_messages_containing_keywords" = "کلمات کلیدی"; "settings_messages_containing_at_room" = "@ اتاق"; "settings_messages_containing_user_name" = "نام کاربری من"; @@ -694,13 +694,13 @@ "settings_encrypted_direct_messages" = "پیام های مستقیم رمزگذاری شده"; "settings_direct_messages" = "پیام مستقیم"; "settings_notify_me_for" = "به من اطلاع بده برای"; -"settings_mentions_and_keywords" = "ذکر و کلمات کلیدی"; +"settings_mentions_and_keywords" = "نام‌بری و کلیدواژگان"; "settings_default" = "اعلان های پیش فرض"; "settings_notifications_disabled_alert_message" = "برای فعال کردن اعلان‌ها، به تنظیمات دستگاه خود بروید."; "settings_notifications_disabled_alert_title" = "غیر فعال کردن اعلان ها"; "settings_pin_rooms_with_missed_notif" = "پین کردن اتاق هایی با اعلان های از دست رفته"; "settings_pin_rooms_with_unread" = "پین کردن اتاق هایی با پیام های خوانده نشده"; -"settings_global_settings_info" = "تنظیمات اعلان جهانی در سرویس گیرنده وب %@ شما موجود است"; +"settings_global_settings_info" = "تنظیمات آگاهی عمومی در کارخواه وب %@تان موجود است"; "settings_show_decrypted_content" = "نمایش محتوای رمزگشایی شده"; "settings_device_notifications" = "اعلان های دستگاه"; "settings_enable_push_notif" = "اعلان ها در این دستگاه"; @@ -1346,3 +1346,4 @@ "onboarding_splash_page_2_title" = "تحت کنترل شماست."; "onboarding_splash_page_1_message" = "یک ارتباط امن و مستقل که سطح حریم شخصی آن دقیقا مشابه ارتباط رو در رو در منزل شماست."; "accessibility_selected" = "انتخاب شده"; +"room_title_members" = "%@ عضو"; diff --git a/Riot/Assets/hu.lproj/Vector.strings b/Riot/Assets/hu.lproj/Vector.strings index 274042639..c965d650c 100644 --- a/Riot/Assets/hu.lproj/Vector.strings +++ b/Riot/Assets/hu.lproj/Vector.strings @@ -2740,3 +2740,18 @@ "key_verification_self_verify_security_upgrade_alert_title" = "Alkalmazás frissítve"; "settings_acceptable_use" = "Elfogadható felhasználói feltételek"; +"room_creation_user_not_found_prompt_title" = "Megerősítés"; +"room_creation_user_not_found_prompt_message" = "Nem található profil ehhez a Matrix-azonosítóhoz. Mindenképp elindítja a privát csevegést?"; +"room_creation_user_not_found_prompt_invite_action" = "Privát csevegés mindenképp"; +"room_participants_invite_unknown_participant_prompt_to_msg" = "Nem található profil ehhez a Matrix-azonosítóhoz. Mindenképp meghívja %@ felhasználót a(z) %@ szobába?"; +"room_participants_invite_anyway" = "Meghívás mindenképp"; + +// Room commands descriptions +"room_command_change_display_name_description" = "Megváltoztatja a megjelenítendő becenevét"; +"room_command_emote_description" = "Megjeleníti a műveletet"; +"room_command_join_room_description" = "Csatlakozik a megadott című szobához"; +"room_command_part_room_description" = "Elhagyja a szobát"; +"room_command_invite_user_description" = "Meghívja az adott azonosítójú felhasználót a jelenlegi szobába"; +"room_command_kick_user_description" = "Eltávolítja az adott azonosítójú felhasználót ebből a szobából"; +"room_command_ban_user_description" = "Kitiltja az adott azonosítójú felhasználót"; +"room_command_unban_user_description" = "Feloldja az adott azonosítójú felhasználó kitiltását"; diff --git a/Riot/Assets/ka.lproj/Vector.strings b/Riot/Assets/ka.lproj/Vector.strings index 8b1378917..8c20e5dc3 100644 --- a/Riot/Assets/ka.lproj/Vector.strings +++ b/Riot/Assets/ka.lproj/Vector.strings @@ -1 +1,77 @@ + +// String for App Store +"store_short_description" = "უსაფრთხო დეცენტრალიზებული ჩატი/ვოიპი"; + +// Titles +"title_home" = "მთავარი"; +"title_favourites" = "ფავორიტები"; +"title_people" = "ხალხი"; +"title_rooms" = "ოთახები"; +"title_groups" = "თემები"; +"warning" = "გაფრთხილება"; + +// Actions +"view" = "ხედი"; +"next" = "შემდეგი"; +"back" = "უკან"; +"continue" = "გაგრძელება"; +"create" = "შექმნა"; +"start" = "აწყება"; +"leave" = "დატოვება"; +"remove" = "წაშლა"; +"retry" = "გამეორება"; +"on" = "ჩართული"; +"off" = "გამორთული"; +"enable" = "ჩართვა"; +"save" = "შენახვა"; +"join" = "შეუერთდება"; +"decline" = "უარყოფა"; +"accept" = "მიღება"; +"camera" = "კამერა"; +"voice" = "ხმა"; +"video" = "ვიდეო"; +"active_call" = "აქტიური ზარი"; +"later" = "შემდეგ"; +"rename" = "გადარქმევა"; +"collapse" = "ჩაკეცვა"; +"send_to" = "გაგზავნა %@-ს"; +"close" = "დახურვა"; +"skip" = "გამოტოვება"; +"joined" = "შეუერთდა"; +"switch" = "გადართვა"; +"more" = "მეტი"; +"less" = "ნაკლები"; +"open" = "გახსნა"; +"private" = "ირადი"; +"public" = "საჯარო"; +"stop" = "შეჩერება"; +"new_word" = "ახალი"; +"existing" = "არსებული"; +"add" = "დამატება"; +"ok" = "კარგი"; +"error" = "შეცდომა"; +"suggest" = "შესთავაზება"; +"confirm" = "დადასტურება"; +"invite_to" = "მიწვევა %@-ში"; + +// Activities +"loading" = "ჩატვირთვა"; +"sending" = "გაგზავნა"; +"callbar_active_and_single_paused" = "1 აქტიური ზარი (%@) · 1 შეჩერებული ზარი"; +"callbar_active_and_multiple_paused" = "1 აქტიური ზარი (%@) · %@ შეჩერებული ზარები"; +"callbar_only_single_paused" = "შეჩერებული ზარი"; +"callbar_return" = "დაბრუნება"; +"store_promotional_text" = "კონფიდენციალობის დაცვით ჩატისა და თანამშრომლობის აპლიკაცია, ღია ქსელზე. დეცენტრალიზებული, რათა მართვა გადაგიცემათ. არაა დათამაინინგი, არაა უკანა კარები და არაა მესამე მხარის წვდომა."; +"invite" = "მიწვევა"; +"cancel" = "გაუქმება"; +"preview" = "წინასწარი ხილვა"; +"active_call_details" = "აქტიური ზარი (%@)"; +"joining" = "შეერთება"; +"done" = "დასრულებული"; +"edit" = "რედაქტირება"; +"saving" = "შენახვა"; + +// Call Bar +"callbar_only_single_active" = "შეეხეთ, რომ დაბრუნდეთ ზარში (%@)"; +"callbar_only_multiple_paused" = "%@ შეჩერებული ზარები"; diff --git a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift index cd129aee3..119366a66 100644 --- a/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift +++ b/Riot/Modules/Analytics/DecryptionFailure+Analytics.swift @@ -21,24 +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, - isFederated: nil, - isMatrixDotOrg: nil, - name: self.reason.errorName, + eventLocalAgeMillis: self.eventLocalAgeMillis, + isFederated: self.isFederated, + isMatrixDotOrg: self.isMatrixOrg, + name: errorName, timeToDecryptMillis: timeToDecryptMillis, - userTrustsOwnIdentity: nil, - wasVisibleToUser: nil + userTrustsOwnIdentity: self.trustOwnIdentityAtTimeOfFailure, + wasVisibleToUser: self.wasVisibleToUser ) } } diff --git a/Riot/Modules/Analytics/DecryptionFailure.swift b/Riot/Modules/Analytics/DecryptionFailure.swift index 9e0ca5785..179509087 100644 --- a/Riot/Modules/Analytics/DecryptionFailure.swift +++ b/Riot/Modules/Analytics/DecryptionFailure.swift @@ -47,6 +47,19 @@ 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? + + /// Is the current user on matrix org + var isMatrixOrg: Bool? + /// Are the sender and recipient on the same homeserver + var isFederated: Bool? + + /// As for now the ios App only reports UTDs visible to user (error are reported from EventFormatter + var wasVisibleToUser: Bool = true + 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 19b8afb19..d76a38ca5 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,24 @@ 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 + + let myDomain = userId.components(separatedBy: ":").last + failure.isMatrixOrg = myDomain == "matrix.org" + + if MXTools.isMatrixUserIdentifier(event.sender) { + let senderDomain = event.sender.components(separatedBy: ":").last + failure.isFederated = senderDomain != nil && senderDomain != myDomain + } + + /// XXX for future work, as for now only the event formatter reports UTDs. That means that it's only UTD ~visible to users + failure.wasVisibleToUser = true + + 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 4494bdbb2..d53a3e19a 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -334,7 +334,7 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; { // 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 7cd9bf480..d37b96f08 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"; + 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) @@ -82,7 +95,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_report_ratcheted_key_utd() { - let myUser = "test@example.com"; + 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) @@ -111,7 +125,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_report_unspecified_error() { - let myUser = "test@example.com"; + 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) @@ -142,7 +157,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_do_not_double_report() { - let myUser = "test@example.com"; + 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) @@ -183,7 +199,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_ignore_not_member() { - let myUser = "test@example.com"; + 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) @@ -217,7 +234,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_notification_center() { - let myUser = "test@example.com"; + 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) @@ -257,7 +275,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_should_report_late_decrypt() { - let myUser = "test@example.com"; + 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) @@ -301,7 +320,8 @@ class DecryptionFailureTrackerTests: XCTestCase { func test_should_report_permanent_decryption_error() { - let myUser = "test@example.com"; + 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,246 @@ 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) + + } + + + func test_should_report_is_matrix_org_and_is_federated() { + + 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.sender = "@bob:example.com" + 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); + + // Simulate succesful decryption after max wait + timeShifter.timestamp = TimeInterval(70) + + decryptionFailureTracker.checkFailures(); + + XCTAssertEqual(testDelegate.reportedFailure?.isMatrixOrg, false); + XCTAssertEqual(testDelegate.reportedFailure?.isFederated, false); + + + let analyticsError = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError.isMatrixDotOrg, false) + XCTAssertEqual(analyticsError.isFederated, false) + + // Report a new error now that session is verified + + let fakeEvent2 = FakeEvent(id: "$0001"); + fakeEvent2.sender = "@bob:example.com" + fakeEvent2.decryptionError = NSError(domain: MXDecryptingErrorDomain, code: Int(MXDecryptingErrorUnknownInboundSessionIdCode.rawValue)) + + fakeSession.mockUserId = "@test:matrix.org"; + fakeRoomState.mockMembers = FakeRoomMembers(joined: [fakeSession.mockUserId]) + + 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?.isMatrixOrg, true); + XCTAssertEqual(testDelegate.reportedFailure?.isFederated, true); + + let analyticsError2 = testDelegate.reportedFailure!.toAnalyticsEvent() + + XCTAssertEqual(analyticsError2.isMatrixDotOrg, true) + XCTAssertEqual(analyticsError2.isFederated, true) + + } + + } diff --git a/RiotTests/FakeUtils.swift b/RiotTests/FakeUtils.swift index 7bd350e4b..f6b1fe477 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) { + + } + +}