mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 06:55:59 +00:00
Add support for reading accounts from Element Classic.
This commit is contained in:
@@ -264,6 +264,7 @@
|
|||||||
2BC579CB5CE90CFE07CA0955 /* EditRoomAddressScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41656BC6267D55C56A2AAC08 /* EditRoomAddressScreenCoordinator.swift */; };
|
2BC579CB5CE90CFE07CA0955 /* EditRoomAddressScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41656BC6267D55C56A2AAC08 /* EditRoomAddressScreenCoordinator.swift */; };
|
||||||
2BEDEA4851E1DF901779362C /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922E498EB74CF6F5CC236F81 /* AdvancedSettingsScreenModels.swift */; };
|
2BEDEA4851E1DF901779362C /* AdvancedSettingsScreenModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922E498EB74CF6F5CC236F81 /* AdvancedSettingsScreenModels.swift */; };
|
||||||
2BFA4C6D5B3D327B02C66AB0 /* TimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4216C12C0369A8AB059EDE9 /* TimelineController.swift */; };
|
2BFA4C6D5B3D327B02C66AB0 /* TimelineController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4216C12C0369A8AB059EDE9 /* TimelineController.swift */; };
|
||||||
|
2C289BECCCBEDBA48DC9AC67 /* ClassicAppMXAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74240BE69DACAD01AA670730 /* ClassicAppMXAccount.swift */; };
|
||||||
2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; };
|
2C4C750D0039AFABDF24236C /* TemplateScreenViewModelProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342BEBC3C5FC3F9943C41C4C /* TemplateScreenViewModelProtocol.swift */; };
|
||||||
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; };
|
2C5E832434EE94E21AB3B238 /* EmojiPickerScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3EAE3E9D5EF4A6D5D9C6CFD /* EmojiPickerScreenViewModel.swift */; };
|
||||||
2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */; };
|
2CA61BB208CD82EBDB58CD13 /* VideoRoomTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0CBEAB5F796BEFBAF7BB6A /* VideoRoomTimelineView.swift */; };
|
||||||
@@ -1323,6 +1324,7 @@
|
|||||||
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
|
E84ADFE9696936C18C2424B5 /* SecureBackupScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A00BB9CD12CF6AC98D5485 /* SecureBackupScreen.swift */; };
|
||||||
E8AE3BE5C5D2E6C91751A2A6 /* StateMachineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC73285743189AE4498A51DD /* StateMachineFactory.swift */; };
|
E8AE3BE5C5D2E6C91751A2A6 /* StateMachineFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC73285743189AE4498A51DD /* StateMachineFactory.swift */; };
|
||||||
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */; };
|
E8B290CBB7E5FF5E3C1B6124 /* KnockRequestsListEmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 627A8B5E798CC778C363655E /* KnockRequestsListEmptyStateView.swift */; };
|
||||||
|
E8BBCFF3B1380F56C73690BC /* ClassicAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39C78D5FF15343724902EB20 /* ClassicAppManager.swift */; };
|
||||||
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */; };
|
E8C4D9F93F0DCED211D5F187 /* HTMLParserStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3877D3CFAC1D33823359BAF /* HTMLParserStyle.swift */; };
|
||||||
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
|
E8C65C19F7C40EE545172DD6 /* RoomScreenFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4137900E28201C314C835C11 /* RoomScreenFooterView.swift */; };
|
||||||
E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; };
|
E9347F56CF0683208F4D9249 /* RoomNotificationSettingsScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81A9B5225D0881CEFA2CF7C9 /* RoomNotificationSettingsScreenViewModel.swift */; };
|
||||||
@@ -1356,6 +1358,7 @@
|
|||||||
EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; };
|
EDF8919F15DE0FF00EF99E70 /* DocumentPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F5567A7EF6F2AB9473236F6 /* DocumentPicker.swift */; };
|
||||||
EE17B7154DCA50677D931A94 /* NavigationTabCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B67DC84B42D86035FCFF6F8 /* NavigationTabCoordinatorTests.swift */; };
|
EE17B7154DCA50677D931A94 /* NavigationTabCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B67DC84B42D86035FCFF6F8 /* NavigationTabCoordinatorTests.swift */; };
|
||||||
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; };
|
EE4E2C1922BBF5169E213555 /* PillAttachmentViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B53D6C5C0D14B04D3AB3F6E /* PillAttachmentViewProvider.swift */; };
|
||||||
|
EE4F6D6F54B8F09F10CA66BE /* ClassicAppAES.swift in Sources */ = {isa = PBXBuildFile; fileRef = 384A744714571BAF138C6B86 /* ClassicAppAES.swift */; };
|
||||||
EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; };
|
EE56238683BC3ECA9BA00684 /* GlobalSearchScreenViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA4D639E27D5882A6A71AECF /* GlobalSearchScreenViewModelTests.swift */; };
|
||||||
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
EE8491AD81F47DF3C192497B /* DecorationTimelineItemProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CF8C196BE143AE226628D /* DecorationTimelineItemProtocol.swift */; };
|
||||||
EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; };
|
EEAE954289DE813A61656AE0 /* LayoutDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C14D83B2B7CD5501A0089EFC /* LayoutDirection.swift */; };
|
||||||
@@ -1406,6 +1409,7 @@
|
|||||||
F4D5A2A8304ED61621BF02D4 /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */; };
|
F4D5A2A8304ED61621BF02D4 /* test_audio.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 66B96842BF5F8ACA1AC84C55 /* test_audio.mp3 */; };
|
||||||
F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; };
|
F519DE17A3A0F760307B2E6D /* InviteUsersScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02D155E09BF961BBA8F85263 /* InviteUsersScreenViewModel.swift */; };
|
||||||
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; };
|
F54E2D6CAD96E1AC15BC526F /* MessageForwardingScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8E60332509665C00179ACF6 /* MessageForwardingScreenViewModel.swift */; };
|
||||||
|
F58BF3BD3233A03F013816E4 /* ClassicAppAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F4DFFFC62187D9A4D2030D /* ClassicAppAccountManager.swift */; };
|
||||||
F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; };
|
F5D2270B5021D521C0D22E11 /* FlowCoordinatorProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B9FCA1CFD07B8CF9BD21266 /* FlowCoordinatorProtocol.swift */; };
|
||||||
F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; };
|
F656F92A63D3DC1978D79427 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 290FDEDA4D764B9F7EBE55A9 /* Algorithms */; };
|
||||||
F65F3909769E7C48B3309D9D /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */; };
|
F65F3909769E7C48B3309D9D /* RemoteSettingsHook.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5D186A6DB8FAC5C9D0E4D61 /* RemoteSettingsHook.swift */; };
|
||||||
@@ -1872,11 +1876,13 @@
|
|||||||
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyMock.swift; sourceTree = "<group>"; };
|
382B50F7E379B3DBBD174364 /* NotificationSettingsProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsProxyMock.swift; sourceTree = "<group>"; };
|
||||||
38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenPINKeypad.swift; sourceTree = "<group>"; };
|
38345442415E07A931197C55 /* AppLockScreenPINKeypad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLockScreenPINKeypad.swift; sourceTree = "<group>"; };
|
||||||
38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModel.swift; sourceTree = "<group>"; };
|
38354164AF59C5006CD05878 /* GlobalSearchScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
384A744714571BAF138C6B86 /* ClassicAppAES.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppAES.swift; sourceTree = "<group>"; };
|
||||||
3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidatorHook.swift; sourceTree = "<group>"; };
|
3865AD7B7249C939D7C69C33 /* CertificateValidatorHook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateValidatorHook.swift; sourceTree = "<group>"; };
|
||||||
38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreen.swift; sourceTree = "<group>"; };
|
38E521D6C2BF8DF0DFB35146 /* DeveloperOptionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeveloperOptionsScreen.swift; sourceTree = "<group>"; };
|
||||||
3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenModelProtocol.swift; sourceTree = "<group>"; };
|
3984C93B8E9B10C92DADF9EE /* RoomDirectorySearchScreenScreenModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomDirectorySearchScreenScreenModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = "<group>"; };
|
398817652FA8ABAE0A31AC6D /* ReadableFrameModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadableFrameModifier.swift; sourceTree = "<group>"; };
|
||||||
39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerMock.swift; sourceTree = "<group>"; };
|
39C0D861FC397AC34BCF089E /* KeychainControllerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainControllerMock.swift; sourceTree = "<group>"; };
|
||||||
|
39C78D5FF15343724902EB20 /* ClassicAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppManager.swift; sourceTree = "<group>"; };
|
||||||
3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = "<group>"; };
|
3A12D3D8138F1B71AFA7C858 /* CompletionSuggestionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionService.swift; sourceTree = "<group>"; };
|
||||||
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitedRoomProxyMock.swift; sourceTree = "<group>"; };
|
3A21027F05874B1BCC3E452B /* InvitedRoomProxyMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvitedRoomProxyMock.swift; sourceTree = "<group>"; };
|
||||||
3AB34956C87731AB094DB33A /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = "<group>"; };
|
3AB34956C87731AB094DB33A /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -2182,6 +2188,7 @@
|
|||||||
73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; };
|
73A5C3F7C9C1DA10CAEC6A98 /* VoiceMessageRecordingComposer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageRecordingComposer.swift; sourceTree = "<group>"; };
|
||||||
73F3153AB9D628110878E24F /* libMatrixRustSDKMocks.a */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = archive.ar; path = libMatrixRustSDKMocks.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
73F3153AB9D628110878E24F /* libMatrixRustSDKMocks.a */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = archive.ar; path = libMatrixRustSDKMocks.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
|
73FEE625AB52042049DB9268 /* ThreadTimelineScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadTimelineScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
|
74240BE69DACAD01AA670730 /* ClassicAppMXAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppMXAccount.swift; sourceTree = "<group>"; };
|
||||||
7447C0AD7EF302CD027D6230 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SAS.strings; sourceTree = "<group>"; };
|
7447C0AD7EF302CD027D6230 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/SAS.strings; sourceTree = "<group>"; };
|
||||||
7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
7463464054DDF194C54F0B04 /* LogViewerScreenViewModelProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerScreenViewModelProtocol.swift; sourceTree = "<group>"; };
|
||||||
74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = "<group>"; };
|
74653BE903970C0E36867D46 /* GlobalSearchScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalSearchScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
@@ -2393,6 +2400,7 @@
|
|||||||
989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = "<group>"; };
|
989FC684408B31A677F5538B /* CompletionSuggestionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionSuggestionView.swift; sourceTree = "<group>"; };
|
||||||
98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinCallButton.swift; sourceTree = "<group>"; };
|
98ABC939BC8F08CA3E967D6C /* JoinCallButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JoinCallButton.swift; sourceTree = "<group>"; };
|
||||||
98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemThreadSummary.swift; sourceTree = "<group>"; };
|
98C6A082F2B2A15E1B9BE280 /* TimelineItemThreadSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemThreadSummary.swift; sourceTree = "<group>"; };
|
||||||
|
98F4DFFFC62187D9A4D2030D /* ClassicAppAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicAppAccountManager.swift; sourceTree = "<group>"; };
|
||||||
997BF045585AF6DB2EBC5755 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
997BF045585AF6DB2EBC5755 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = "<group>"; };
|
9A008E57D52B07B78DFAD1BB /* RoomFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomFlowCoordinator.swift; sourceTree = "<group>"; };
|
||||||
9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; };
|
9A028783CFFF861C5E44FFB1 /* BadgeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeLabel.swift; sourceTree = "<group>"; };
|
||||||
@@ -3982,6 +3990,13 @@
|
|||||||
path = SupportingFiles;
|
path = SupportingFiles;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4001FC7CD65776AB3745245C /* ClassicAppAccountConfirmationScreen */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
);
|
||||||
|
path = ClassicAppAccountConfirmationScreen;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4044C040B64B9F077298C947 /* View */ = {
|
4044C040B64B9F077298C947 /* View */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -4300,6 +4315,17 @@
|
|||||||
path = ReportRoomScreen;
|
path = ReportRoomScreen;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4E126BB8215781BE1A126743 /* ClassicApp */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
98F4DFFFC62187D9A4D2030D /* ClassicAppAccountManager.swift */,
|
||||||
|
384A744714571BAF138C6B86 /* ClassicAppAES.swift */,
|
||||||
|
39C78D5FF15343724902EB20 /* ClassicAppManager.swift */,
|
||||||
|
74240BE69DACAD01AA670730 /* ClassicAppMXAccount.swift */,
|
||||||
|
);
|
||||||
|
path = ClassicApp;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4EC4EBBC4F6885775F198875 /* Sources */ = {
|
4EC4EBBC4F6885775F198875 /* Sources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5790,6 +5816,7 @@
|
|||||||
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */,
|
F3A1AB5A84D843B6AC8D5F1E /* AuthenticationService.swift */,
|
||||||
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */,
|
5E75948AA1FE1D1A7809931F /* AuthenticationServiceProtocol.swift */,
|
||||||
007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */,
|
007C16779FDCF10DA4F1A510 /* LinkNewDeviceService.swift */,
|
||||||
|
4E126BB8215781BE1A126743 /* ClassicApp */,
|
||||||
);
|
);
|
||||||
path = Authentication;
|
path = Authentication;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -6591,6 +6618,7 @@
|
|||||||
children = (
|
children = (
|
||||||
92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */,
|
92390F9FA98255440A6BF5F8 /* OIDCAuthenticationPresenter.swift */,
|
||||||
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */,
|
9E6D88E8AFFBF2C1D589C0FA /* UIConstants.swift */,
|
||||||
|
4001FC7CD65776AB3745245C /* ClassicAppAccountConfirmationScreen */,
|
||||||
90F48FEF84016ED42A94BA24 /* LoginScreen */,
|
90F48FEF84016ED42A94BA24 /* LoginScreen */,
|
||||||
BA1938A75D8C780F694CEB62 /* ServerConfirmationScreen */,
|
BA1938A75D8C780F694CEB62 /* ServerConfirmationScreen */,
|
||||||
2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */,
|
2D0D49B0533C4C2EB889BF3A /* ServerSelectionScreen */,
|
||||||
@@ -7984,6 +8012,10 @@
|
|||||||
A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */,
|
A076E0A9338FD2D950C3C4A1 /* ChatsSpaceFiltersScreenViewModelProtocol.swift in Sources */,
|
||||||
572474C7CA4B03FF0B5DF548 /* ChatsTabFlowCoordinator.swift in Sources */,
|
572474C7CA4B03FF0B5DF548 /* ChatsTabFlowCoordinator.swift in Sources */,
|
||||||
BC5F94B10B40ABEC6046B473 /* ChatsTabFlowCoordinatorStateMachine.swift in Sources */,
|
BC5F94B10B40ABEC6046B473 /* ChatsTabFlowCoordinatorStateMachine.swift in Sources */,
|
||||||
|
EE4F6D6F54B8F09F10CA66BE /* ClassicAppAES.swift in Sources */,
|
||||||
|
F58BF3BD3233A03F013816E4 /* ClassicAppAccountManager.swift in Sources */,
|
||||||
|
2C289BECCCBEDBA48DC9AC67 /* ClassicAppMXAccount.swift in Sources */,
|
||||||
|
E8BBCFF3B1380F56C73690BC /* ClassicAppManager.swift in Sources */,
|
||||||
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */,
|
A52090A4FE0DB826578DFC03 /* Client.swift in Sources */,
|
||||||
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */,
|
C80E06ED97CE52704A46C148 /* ClientBuilder.swift in Sources */,
|
||||||
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */,
|
87CEA3E07B602705BC2D2A20 /* ClientBuilderHook.swift in Sources */,
|
||||||
@@ -9398,6 +9430,9 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
|
||||||
|
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
|
||||||
|
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
@@ -9468,6 +9503,9 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CLASSIC_APP_GROUP_IDENTIFIER = group.im.vector;
|
||||||
|
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER = "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared";
|
||||||
|
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER = "im.vector.app.encryption-manager-service";
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
|||||||
@@ -605,8 +605,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
private func startAuthentication() {
|
private func startAuthentication() {
|
||||||
let encryptionKeyProvider = EncryptionKeyProvider()
|
let encryptionKeyProvider = EncryptionKeyProvider()
|
||||||
|
let classicAppManager = makeClassicAppManager()
|
||||||
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
||||||
encryptionKeyProvider: encryptionKeyProvider,
|
encryptionKeyProvider: encryptionKeyProvider,
|
||||||
|
classicAppManager: classicAppManager,
|
||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
appHooks: appHooks)
|
appHooks: appHooks)
|
||||||
|
|
||||||
@@ -628,6 +630,19 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func makeClassicAppManager() -> ClassicAppManagerProtocol? {
|
||||||
|
guard let classicAppGroupIdentifier = InfoPlistReader.main.classicAppGroupIdentifier,
|
||||||
|
let classicAppKeychainServiceIdentifier = InfoPlistReader.main.classicAppKeychainServiceIdentifier,
|
||||||
|
let classicAppKeychainAccessGroupIdentifier = InfoPlistReader.main.classicAppKeychainAccessGroupIdentifier else {
|
||||||
|
MXLog.info("Classic App IDs not set, manager disabled.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClassicAppManager(classicAppGroupIdentifier: classicAppGroupIdentifier,
|
||||||
|
classicAppKeychainServiceIdentifier: classicAppKeychainServiceIdentifier,
|
||||||
|
classicAppKeychainAccessGroupIdentifier: classicAppKeychainAccessGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
private func runPostSessionSetupTasks() async {
|
private func runPostSessionSetupTasks() async {
|
||||||
guard let userSession, let userSessionFlowCoordinator else {
|
guard let userSession, let userSessionFlowCoordinator else {
|
||||||
fatalError("User session not setup")
|
fatalError("User session not setup")
|
||||||
@@ -660,6 +675,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
|
|||||||
|
|
||||||
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
let authenticationService = AuthenticationService(userSessionStore: userSessionStore,
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: makeClassicAppManager(),
|
||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
appHooks: appHooks)
|
appHooks: appHooks)
|
||||||
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login)
|
_ = await authenticationService.configure(for: userSession.clientProxy.homeserver, flow: .login)
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ struct InfoPlistReader {
|
|||||||
static let bundleURLTypes = "CFBundleURLTypes"
|
static let bundleURLTypes = "CFBundleURLTypes"
|
||||||
static let bundleURLName = "CFBundleURLName"
|
static let bundleURLName = "CFBundleURLName"
|
||||||
static let bundleURLSchemes = "CFBundleURLSchemes"
|
static let bundleURLSchemes = "CFBundleURLSchemes"
|
||||||
|
|
||||||
|
static let classicAppGroupIdentifier = "classicAppGroupIdentifier"
|
||||||
|
static let classicAppKeychainServiceIdentifier = "classicAppKeychainServiceIdentifier"
|
||||||
|
static let classicAppKeychainAccessGroupIdentifier = "classicAppKeychainAccessGroupIdentifier"
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum Values {
|
private enum Values {
|
||||||
@@ -115,8 +119,23 @@ struct InfoPlistReader {
|
|||||||
return utType.lowercased()
|
return utType.lowercased()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Sign in with Classic app
|
||||||
|
|
||||||
|
var classicAppGroupIdentifier: String? {
|
||||||
|
infoPlistValue(forKey: Keys.classicAppGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
var classicAppKeychainServiceIdentifier: String? {
|
||||||
|
infoPlistValue(forKey: Keys.classicAppKeychainServiceIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
var classicAppKeychainAccessGroupIdentifier: String? {
|
||||||
|
infoPlistValue(forKey: Keys.classicAppKeychainAccessGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
@_disfavoredOverload // Make sure optional types default to the optional version below.
|
||||||
private func infoPlistValue<T>(forKey key: String) -> T {
|
private func infoPlistValue<T>(forKey key: String) -> T {
|
||||||
guard let result = bundle.object(forInfoDictionaryKey: key) as? T else {
|
guard let result = bundle.object(forInfoDictionaryKey: key) as? T else {
|
||||||
fatalError("Add \(key) into your target's Info.plst")
|
fatalError("Add \(key) into your target's Info.plst")
|
||||||
@@ -124,6 +143,10 @@ struct InfoPlistReader {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func infoPlistValue<T>(forKey key: String) -> T? {
|
||||||
|
bundle.object(forInfoDictionaryKey: key) as? T
|
||||||
|
}
|
||||||
|
|
||||||
private func customSchemeForName(_ name: String) -> String {
|
private func customSchemeForName(_ name: String) -> String {
|
||||||
let urlTypes: [[String: Any]] = infoPlistValue(forKey: Keys.bundleURLTypes)
|
let urlTypes: [[String: Any]] = infoPlistValue(forKey: Keys.bundleURLTypes)
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -13,7 +13,7 @@ import SwiftUI
|
|||||||
struct AuthenticationStartScreen: View {
|
struct AuthenticationStartScreen: View {
|
||||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||||
|
|
||||||
let context: AuthenticationStartScreenViewModel.Context
|
@Bindable var context: AuthenticationStartScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
@@ -51,6 +51,7 @@ struct AuthenticationStartScreen: View {
|
|||||||
.background {
|
.background {
|
||||||
AuthenticationStartScreenBackgroundImage()
|
AuthenticationStartScreenBackgroundImage()
|
||||||
}
|
}
|
||||||
|
.alert(item: $context.alertInfo)
|
||||||
.introspect(.window, on: .supportedVersions) { window in
|
.introspect(.window, on: .supportedVersions) { window in
|
||||||
context.send(viewAction: .updateWindow(window))
|
context.send(viewAction: .updateWindow(window))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
private var sessionDirectories: SessionDirectories
|
private var sessionDirectories: SessionDirectories
|
||||||
private let passphrase: String
|
private let passphrase: String
|
||||||
|
|
||||||
private let clientFactory: AuthenticationClientFactoryProtocol
|
|
||||||
private let userSessionStore: UserSessionStoreProtocol
|
private let userSessionStore: UserSessionStoreProtocol
|
||||||
|
private let classicAppManager: ClassicAppManagerProtocol?
|
||||||
|
private let clientFactory: AuthenticationClientFactoryProtocol
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
private let appHooks: AppHooks
|
private let appHooks: AppHooks
|
||||||
|
|
||||||
@@ -29,16 +30,31 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
|||||||
|
|
||||||
init(userSessionStore: UserSessionStoreProtocol,
|
init(userSessionStore: UserSessionStoreProtocol,
|
||||||
encryptionKeyProvider: EncryptionKeyProviderProtocol,
|
encryptionKeyProvider: EncryptionKeyProviderProtocol,
|
||||||
|
classicAppManager: ClassicAppManagerProtocol?,
|
||||||
clientFactory: AuthenticationClientFactoryProtocol = AuthenticationClientFactory(),
|
clientFactory: AuthenticationClientFactoryProtocol = AuthenticationClientFactory(),
|
||||||
appSettings: AppSettings,
|
appSettings: AppSettings,
|
||||||
appHooks: AppHooks) {
|
appHooks: AppHooks) {
|
||||||
sessionDirectories = .init()
|
sessionDirectories = .init()
|
||||||
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
|
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
|
||||||
self.clientFactory = clientFactory
|
|
||||||
self.userSessionStore = userSessionStore
|
self.userSessionStore = userSessionStore
|
||||||
|
self.classicAppManager = classicAppManager
|
||||||
|
self.clientFactory = clientFactory
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
self.appHooks = appHooks
|
self.appHooks = appHooks
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let classicAppManager {
|
||||||
|
// Just let the app manager log the detected account for now.
|
||||||
|
_ = try classicAppManager.loadAccounts()
|
||||||
|
} else {
|
||||||
|
MXLog.info("Classic App not configured, skipping loadAccounts.")
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// This should show an alert: "We have detected an older version of Element Classic, but no bueno!"
|
||||||
|
MXLog.error("Failed loading accounts from the Classic app: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
// When updating these, don't forget to update the reset method too.
|
// When updating these, don't forget to update the reset method too.
|
||||||
homeserverSubject = .init(LoginHomeserver(address: appSettings.accountProviders[0], loginMode: .unknown))
|
homeserverSubject = .init(LoginHomeserver(address: appSettings.accountProviders[0], loginMode: .unknown))
|
||||||
flow = .login
|
flow = .login
|
||||||
@@ -277,6 +293,7 @@ extension AuthenticationService {
|
|||||||
static var mock: AuthenticationService {
|
static var mock: AuthenticationService {
|
||||||
AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: AuthenticationClientFactoryMock(configuration: .init()),
|
clientFactory: AuthenticationClientFactoryMock(configuration: .init()),
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CommonCrypto
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum ClassicAppAES {
|
||||||
|
enum Error: Swift.Error {
|
||||||
|
case cannotInitializeCryptor
|
||||||
|
case decryptionFailed(CCCryptorStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt data using AES-256 in CTR mode.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - data: The data to decrypt
|
||||||
|
/// - aesKey: The AES decryption key (256-bit)
|
||||||
|
/// - iv: The initialization vector
|
||||||
|
/// - Returns: The decrypted data, or nil if decryption fails
|
||||||
|
/// - Throws: An error if decryption fails
|
||||||
|
static func decrypt(_ data: Data, aesKey: Data, iv: Data) throws -> Data {
|
||||||
|
var cryptor: CCCryptorRef?
|
||||||
|
var status: CCCryptorStatus
|
||||||
|
|
||||||
|
// Create the cryptor
|
||||||
|
status = iv.withUnsafeBytes { ivBytes in
|
||||||
|
aesKey.withUnsafeBytes { keyBytes in
|
||||||
|
CCCryptorCreateWithMode(CCOperation(kCCDecrypt),
|
||||||
|
CCMode(kCCModeCTR),
|
||||||
|
CCAlgorithm(kCCAlgorithmAES),
|
||||||
|
CCPadding(ccNoPadding),
|
||||||
|
ivBytes.baseAddress,
|
||||||
|
keyBytes.baseAddress,
|
||||||
|
kCCKeySizeAES256,
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CCModeOptions(kCCModeOptionCTR_BE),
|
||||||
|
&cryptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard status == kCCSuccess, let cryptor else {
|
||||||
|
MXLog.error("Failed to create cryptor: \(status)")
|
||||||
|
throw Error.cannotInitializeCryptor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the output buffer size
|
||||||
|
let bufferLength = CCCryptorGetOutputLength(cryptor, data.count, false)
|
||||||
|
var buffer = Data(count: bufferLength)
|
||||||
|
|
||||||
|
var outLength = 0
|
||||||
|
status = data.withUnsafeBytes { dataBytes in
|
||||||
|
let count = buffer.count
|
||||||
|
return buffer.withUnsafeMutableBytes { bufferBytes in
|
||||||
|
CCCryptorUpdate(cryptor,
|
||||||
|
dataBytes.baseAddress,
|
||||||
|
data.count,
|
||||||
|
bufferBytes.baseAddress,
|
||||||
|
count,
|
||||||
|
&outLength)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let releaseStatus = CCCryptorRelease(cryptor)
|
||||||
|
status = (status == kCCSuccess && releaseStatus == kCCSuccess) ? CCCryptorStatus(kCCSuccess) : status
|
||||||
|
|
||||||
|
guard status == kCCSuccess else {
|
||||||
|
MXLog.error("Decryption failed: \(status)")
|
||||||
|
throw Error.decryptionFailed(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ClassicAppAccountManager {
|
||||||
|
static let matrixKitFolder = "MatrixKit"
|
||||||
|
static let kMXKAccountsKey = "accountsV2"
|
||||||
|
static let kMXFileStoreFolder = "MXFileStore"
|
||||||
|
static let kMXFileStoreUsersFolder = "users"
|
||||||
|
static let cryptoStoreFolder = "MXCryptoStore"
|
||||||
|
|
||||||
|
let cacheFolder: URL
|
||||||
|
let iv: Data
|
||||||
|
let aesKey: Data
|
||||||
|
|
||||||
|
private(set) var accounts: [ClassicAppMXAccount] = []
|
||||||
|
private var users: [String: ClassicAppMXUser] = [:]
|
||||||
|
|
||||||
|
var activeAccounts: [ClassicAppAccount] {
|
||||||
|
accounts
|
||||||
|
.filter { !$0.isDisabled && !$0.isSoftLogout }
|
||||||
|
.compactMap(activeAccount)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(cacheFolder: URL, iv: Data, aesKey: Data) {
|
||||||
|
self.cacheFolder = cacheFolder
|
||||||
|
self.iv = iv
|
||||||
|
self.aesKey = aesKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the path of the file containing stored MXAccounts array
|
||||||
|
func accountFile() -> URL {
|
||||||
|
cacheFolder.appending(component: Self.matrixKitFolder).appending(component: Self.kMXKAccountsKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAccounts() {
|
||||||
|
MXLog.info("Loading accounts from Classic app.")
|
||||||
|
let accountFile = accountFile()
|
||||||
|
if FileManager.default.fileExists(atPath: accountFile.path(percentEncoded: false)) {
|
||||||
|
let startDate = Date()
|
||||||
|
|
||||||
|
do {
|
||||||
|
let fileContent = try Data(contentsOf: accountFile, options: [.alwaysMapped, .uncached])
|
||||||
|
|
||||||
|
// Decrypt data if encryption method is provided
|
||||||
|
let unciphered = try ClassicAppAES.decrypt(fileContent, aesKey: aesKey, iv: iv)
|
||||||
|
let decoder = NSKeyedUnarchiver(forReadingWith: unciphered)
|
||||||
|
decoder.setClass(ClassicAppMXAccount.self, forClassName: "MXKAccount")
|
||||||
|
decoder.setClass(ClassicAppMXThirdPartyIdentifier.self, forClassName: "MXThirdPartyIdentifier")
|
||||||
|
decoder.setClass(ClassicAppMXDevice.self, forClassName: "MXDevice")
|
||||||
|
|
||||||
|
guard let accounts = decoder.decodeObject(forKey: "mxAccounts") as? [ClassicAppMXAccount] else {
|
||||||
|
MXLog.error("Failed to decode accounts.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.accounts = accounts
|
||||||
|
|
||||||
|
MXLog.info("[MXKAccountManager] loadAccounts. \(accounts.count) accounts loaded in \(Date().timeIntervalSince(startDate) * 1000)ms")
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to load account file: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
for account in activeAccounts {
|
||||||
|
if let user = loadUsers([account.userID], forAccount: account.userID).first {
|
||||||
|
users[user.userID] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if accounts.isEmpty {
|
||||||
|
MXLog.info("[MXKAccountManager] loadAccounts. No accounts")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// From `MXCryptoMachineStore`
|
||||||
|
func cryptoStoreURL(for userID: String) -> URL {
|
||||||
|
cacheFolder.appending(component: Self.cryptoStoreFolder).appending(component: userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileStorePath: URL {
|
||||||
|
cacheFolder.appending(component: Self.kMXFileStoreFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePath(for userID: String) -> URL {
|
||||||
|
fileStorePath.appending(component: userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This store contains all of the users known to the specific user ID.
|
||||||
|
func storeUsersPath(for userID: String) -> URL {
|
||||||
|
storePath(for: userID).appending(component: Self.kMXFileStoreUsersFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadUsers(_ userIDs: [String], forAccount accountUserID: String) -> [ClassicAppMXUser] {
|
||||||
|
// Determine which groups to load based on userIds
|
||||||
|
var groups: [String: [String]] = [:]
|
||||||
|
for userID in userIDs {
|
||||||
|
let groupID = String(userID.hash % 100)
|
||||||
|
|
||||||
|
if groups[groupID] != nil {
|
||||||
|
groups[groupID]?.append(userID)
|
||||||
|
} else {
|
||||||
|
groups[groupID] = [userID]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let usersFolder = storeUsersPath(for: accountUserID)
|
||||||
|
|
||||||
|
var loadedUsers: [ClassicAppMXUser] = []
|
||||||
|
for group in groups.keys {
|
||||||
|
autoreleasepool {
|
||||||
|
let groupFile = usersFolder.appendingPathComponent(group)
|
||||||
|
|
||||||
|
// Load stored users in this group
|
||||||
|
do {
|
||||||
|
let fileContent = try Data(contentsOf: groupFile)
|
||||||
|
|
||||||
|
let decoder = NSKeyedUnarchiver(forReadingWith: fileContent)
|
||||||
|
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser")
|
||||||
|
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser")
|
||||||
|
|
||||||
|
if let groupUsers = decoder.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? [String: ClassicAppMXUser] {
|
||||||
|
let usersToLoad = Set(groups[group] ?? [])
|
||||||
|
for user in groupUsers.values where usersToLoad.contains(user.userID) {
|
||||||
|
loadedUsers.append(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.warning("[MXFileStore] Warning: MXFileRoomStore file for users group \(group) has been corrupted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
private func activeAccount(mxAccount: ClassicAppMXAccount) -> ClassicAppAccount? {
|
||||||
|
guard let userID = mxAccount.credentials.userID, let serverName = serverName(for: userID) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClassicAppAccount(userID: userID,
|
||||||
|
displayName: users[userID]?.displayName,
|
||||||
|
serverName: serverName,
|
||||||
|
cryptoStoreURL: cryptoStoreURL(for: userID))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The server name extracted from the user's ID.
|
||||||
|
private func serverName(for userID: String) -> String? {
|
||||||
|
#warning("Expose a serverName method for this from the SDK?")
|
||||||
|
let components = userID.components(separatedBy: ":")
|
||||||
|
guard components.count > 1 else { return nil }
|
||||||
|
return components[1] // Directly use [1] as .last may be the port number.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Probably not needed
|
||||||
|
|
||||||
|
private extension ClassicAppAccountManager {
|
||||||
|
func loadUsers(forAccount accountUserID: String) {
|
||||||
|
let startDate = Date()
|
||||||
|
var users: [String: ClassicAppMXUser] = [:]
|
||||||
|
|
||||||
|
// Load all users which are distributed in several files
|
||||||
|
let storeUsersPath = storeUsersPath(for: accountUserID)
|
||||||
|
let groups = try? FileManager.default.contentsOfDirectory(atPath: storeUsersPath.path(percentEncoded: false))
|
||||||
|
|
||||||
|
if let groups {
|
||||||
|
for group in groups {
|
||||||
|
let groupFile = storeUsersPath.appending(path: group)
|
||||||
|
|
||||||
|
// Load stored users in this group
|
||||||
|
do {
|
||||||
|
let fileContent = try Data(contentsOf: groupFile)
|
||||||
|
|
||||||
|
let decoder = NSKeyedUnarchiver(forReadingWith: fileContent)
|
||||||
|
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser")
|
||||||
|
decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser")
|
||||||
|
|
||||||
|
if let groupUsers = decoder.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? [String: ClassicAppMXUser] {
|
||||||
|
// Append them
|
||||||
|
users.merge(groupUsers) { _, new in new }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed to load users from group \(group): \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MXLog.debug("[MXFileStore] Loaded \(users.count) MXUsers in \(Date().timeIntervalSince(startDate) * 1000)ms")
|
||||||
|
|
||||||
|
self.users = users
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ClassicAppAccount {
|
||||||
|
let userID: String
|
||||||
|
let displayName: String?
|
||||||
|
let serverName: String
|
||||||
|
let cryptoStoreURL: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: NSCoding Types
|
||||||
|
|
||||||
|
class ClassicAppMXAccount: NSObject, NSCoding {
|
||||||
|
/// The account's credentials: homeserver, access token, user ID.
|
||||||
|
private(set) var credentials: Credentials
|
||||||
|
/// The identity server URL.
|
||||||
|
var identityServerURL: String
|
||||||
|
/// The antivirus server URL, if any (nil by default).
|
||||||
|
/// Set a non-null url to configure the antivirus scanner use.
|
||||||
|
var antivirusServerURL: String?
|
||||||
|
/// The Push Gateway URL used to send event notifications to (nil by default).
|
||||||
|
/// This URL should be over HTTPS and never over HTTP.
|
||||||
|
var pushGatewayURL: String?
|
||||||
|
/// The 3PIDs linked to this account.
|
||||||
|
/// [self load3PIDs] must be called to update the property.
|
||||||
|
private(set) var threePIDs: [ClassicAppMXThirdPartyIdentifier]?
|
||||||
|
/// The account user's device.
|
||||||
|
/// [self loadDeviceInformation] must be called to update the property.
|
||||||
|
private(set) var device: ClassicAppMXDevice?
|
||||||
|
/// Transient information storage.
|
||||||
|
private(set) var others = NSMutableDictionary()
|
||||||
|
/// Flag to indicate that an APNS pusher has been set on the homeserver for this device.
|
||||||
|
private(set) var hasPusherForPushNotifications = false
|
||||||
|
|
||||||
|
/// The Push notification activity (based on PushKit) for this account.
|
||||||
|
/// YES when Push is turned on (locally available and enabled homeserver side).
|
||||||
|
var isPushKitNotificationActive: Bool {
|
||||||
|
// This would typically have custom getter logic
|
||||||
|
hasPusherForPushKitNotifications
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flag to indicate that a PushKit pusher has been set on the homeserver for this device.
|
||||||
|
private(set) var hasPusherForPushKitNotifications = false
|
||||||
|
/// Enable In-App notifications based on Remote notifications rules.
|
||||||
|
/// NO by default.
|
||||||
|
var enableInAppNotifications = false
|
||||||
|
/// Disable the account without logging out (NO by default).
|
||||||
|
///
|
||||||
|
/// A matrix session is automatically opened for the account when this property is toggled from YES to NO.
|
||||||
|
/// The session is closed when this property is set to YES.
|
||||||
|
var isDisabled = false
|
||||||
|
/// Flag indicating if the end user has been warned about encryption and its limitations.
|
||||||
|
var isWarnedAboutEncryption = false
|
||||||
|
|
||||||
|
/// Flag to indicate if the account has been logged out by the homeserver admin.
|
||||||
|
private(set) var isSoftLogout = false
|
||||||
|
|
||||||
|
// MARK: NSCoding
|
||||||
|
|
||||||
|
enum Keys {
|
||||||
|
static let homeserverURL = "homeserverurl" // String?
|
||||||
|
static let userID = "userid" // String?
|
||||||
|
static let accessToken = "accesstoken" // String?
|
||||||
|
static let accessTokenExpiresAt = "accessTokenExpiresAt" // UInt64
|
||||||
|
static let refreshToken = "refreshToken" // String?
|
||||||
|
static let identityServerURL = "identityserverurl" // String?
|
||||||
|
static let identityServerAccessToken = "identityserveraccesstoken" // String?
|
||||||
|
static let deviceID = "deviceId" // String?
|
||||||
|
static let allowedCertificate = "allowedCertificate" // Data?
|
||||||
|
static let threePIDs = "threePIDs" // [MXThirdPartyIdentifier]?
|
||||||
|
static let device = "device" // MXDevice?
|
||||||
|
static let antivirusServerURL = "antivirusserverurl" // String?
|
||||||
|
static let pushGatewayURL = "pushgatewayurl" // String?
|
||||||
|
static let hasPusherForPushNotifications = "_enablePushNotifications" // Bool
|
||||||
|
static let hasPusherForPushKitNotifications = "enablePushKitNotifications" // Bool
|
||||||
|
static let enableInAppNotifications = "enableInAppNotifications" // Bool
|
||||||
|
static let isDisabled = "disabled" // Bool
|
||||||
|
static let isSoftLogout = "isSoftLogout" // Bool
|
||||||
|
static let isWarnedAboutEncryption = "warnedAboutEncryption" // Bool
|
||||||
|
static let others = "others" // NSMutableDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
let homeserverURL = coder.decodeObject(forKey: Keys.homeserverURL) as? String
|
||||||
|
let userID = coder.decodeObject(forKey: Keys.userID) as? String
|
||||||
|
let accessToken = coder.decodeObject(forKey: Keys.accessToken) as? String
|
||||||
|
|
||||||
|
credentials = Credentials(homeserver: homeserverURL,
|
||||||
|
userID: userID,
|
||||||
|
accessToken: accessToken)
|
||||||
|
|
||||||
|
credentials.accessTokenExpiresAt = UInt64(coder.decodeInt64(forKey: Keys.accessTokenExpiresAt))
|
||||||
|
credentials.refreshToken = coder.decodeObject(forKey: Keys.refreshToken) as? String
|
||||||
|
credentials.identityServer = coder.decodeObject(forKey: Keys.identityServerURL) as? String
|
||||||
|
credentials.identityServerAccessToken = coder.decodeObject(forKey: Keys.identityServerAccessToken) as? String
|
||||||
|
credentials.deviceID = coder.decodeObject(forKey: Keys.deviceID) as? String
|
||||||
|
credentials.allowedCertificate = coder.decodeObject(forKey: Keys.allowedCertificate) as? Data
|
||||||
|
|
||||||
|
identityServerURL = credentials.identityServer ?? ""
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
if let threePIDs = coder.decodeObject(forKey: Keys.threePIDs) as? [ClassicAppMXThirdPartyIdentifier] {
|
||||||
|
self.threePIDs = threePIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if let device = coder.decodeObject(forKey: Keys.device) as? ClassicAppMXDevice {
|
||||||
|
self.device = device
|
||||||
|
}
|
||||||
|
|
||||||
|
if let antivirusServerURL = coder.decodeObject(forKey: Keys.antivirusServerURL) as? String {
|
||||||
|
self.antivirusServerURL = antivirusServerURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pushGatewayURL = coder.decodeObject(forKey: Keys.pushGatewayURL) as? String {
|
||||||
|
self.pushGatewayURL = pushGatewayURL
|
||||||
|
}
|
||||||
|
|
||||||
|
hasPusherForPushNotifications = coder.decodeBool(forKey: Keys.hasPusherForPushNotifications)
|
||||||
|
hasPusherForPushKitNotifications = coder.decodeBool(forKey: Keys.hasPusherForPushKitNotifications)
|
||||||
|
enableInAppNotifications = coder.decodeBool(forKey: Keys.enableInAppNotifications)
|
||||||
|
|
||||||
|
isDisabled = coder.decodeBool(forKey: Keys.isDisabled)
|
||||||
|
isSoftLogout = coder.decodeBool(forKey: Keys.isSoftLogout)
|
||||||
|
|
||||||
|
isWarnedAboutEncryption = coder.decodeBool(forKey: Keys.isWarnedAboutEncryption)
|
||||||
|
|
||||||
|
if let others = coder.decodeObject(forKey: Keys.others) as? NSMutableDictionary {
|
||||||
|
self.others = others
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
fatalError("Not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `MXCredentials` struct contains credentials to communicate with the Matrix
|
||||||
|
/// Client-Server API.
|
||||||
|
struct Credentials {
|
||||||
|
/// The homeserver url (ex: "https://matrix.org").
|
||||||
|
var homeserver: String?
|
||||||
|
/// The identity server url (ex: "https://vector.im").
|
||||||
|
var identityServer: String?
|
||||||
|
/// The obtained user ID.
|
||||||
|
var userID: String?
|
||||||
|
/// The access token to create a MXRestClient
|
||||||
|
var accessToken: String?
|
||||||
|
/// The timestamp in milliseconds for when the access token will expire
|
||||||
|
var accessTokenExpiresAt: UInt64 = 0
|
||||||
|
/// The refresh token, which can be used to obtain new access tokens. (optional)
|
||||||
|
var refreshToken: String?
|
||||||
|
/// The access token to create a MXIdentityServerRestClient
|
||||||
|
var identityServerAccessToken: String?
|
||||||
|
/// The device ID.
|
||||||
|
var deviceID: String?
|
||||||
|
/// The server certificate trusted by the user (nil when the server is trusted by the device).
|
||||||
|
var allowedCertificate: Data?
|
||||||
|
/// The ignored server certificate (set when the user ignores a certificate change).
|
||||||
|
var ignoredCertificate: Data?
|
||||||
|
/// Additional data received during login process
|
||||||
|
var loginOthers: [String: Any]?
|
||||||
|
|
||||||
|
init(homeserver: String?, userID: String?, accessToken: String?) {
|
||||||
|
self.homeserver = homeserver
|
||||||
|
self.userID = userID
|
||||||
|
self.accessToken = accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `MXThirdPartyIdentifier` represents the response to /account/3pid GET request.
|
||||||
|
class ClassicAppMXThirdPartyIdentifier: NSObject, NSCoding {
|
||||||
|
/// The medium of the third party identifier.
|
||||||
|
var medium: String
|
||||||
|
/// The third party identifier address.
|
||||||
|
var address: String
|
||||||
|
/// The timestamp in milliseconds when this 3PID has been validated.
|
||||||
|
var validatedAt: UInt64
|
||||||
|
/// The timestamp in milliseconds when this 3PID has been added to the user account.
|
||||||
|
var addedAt: UInt64
|
||||||
|
|
||||||
|
// MARK: NSCoding
|
||||||
|
|
||||||
|
enum Keys {
|
||||||
|
static let medium = "medium" // String
|
||||||
|
static let address = "address" // String
|
||||||
|
static let validatedAt = "validatedAt" // NSNumber?.uint64Value
|
||||||
|
static let addedAt = "addedAt" // NSNumber?.uint64Value
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
guard let medium = aDecoder.decodeObject(forKey: Keys.medium) as? String,
|
||||||
|
let address = aDecoder.decodeObject(forKey: Keys.address) as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.medium = medium
|
||||||
|
self.address = address
|
||||||
|
|
||||||
|
if let validatedAtNumber = aDecoder.decodeObject(forKey: Keys.validatedAt) as? NSNumber {
|
||||||
|
validatedAt = validatedAtNumber.uint64Value
|
||||||
|
} else {
|
||||||
|
validatedAt = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if let addedAtNumber = aDecoder.decodeObject(forKey: Keys.addedAt) as? NSNumber {
|
||||||
|
addedAt = addedAtNumber.uint64Value
|
||||||
|
} else {
|
||||||
|
addedAt = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
fatalError("Not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `MXDevice` represents a device of the current user.
|
||||||
|
class ClassicAppMXDevice: NSObject, NSCoding {
|
||||||
|
/// A unique identifier of the device.
|
||||||
|
var deviceID: String
|
||||||
|
/// The display name set by the user for this device. Absent if no name has been set.
|
||||||
|
var displayName: String?
|
||||||
|
/// The IP address where this device was last seen. (May be a few minutes out of date, for efficiency reasons).
|
||||||
|
var lastSeenIP: String?
|
||||||
|
/// The timestamp (in milliseconds since the unix epoch) when this devices was last seen. (May be a few minutes out of date, for efficiency reasons).
|
||||||
|
var lastSeenTimestamp: UInt64
|
||||||
|
/// The latest recorded user agent for the device.
|
||||||
|
var lastSeenUserAgent: String?
|
||||||
|
|
||||||
|
// MARK: NSCoding
|
||||||
|
|
||||||
|
enum Keys {
|
||||||
|
static let deviceID = "device_id" // String
|
||||||
|
static let displayName = "display_name" // String?
|
||||||
|
static let lastSeenIP = "last_seen_ip" // String?
|
||||||
|
static let lastSeenTimestamp = "last_seen_ts" // NSNumber?.uint64Value
|
||||||
|
static let lastSeenUserAgent = "org.matrix.msc3852.last_seen_user_agent" // String?
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
guard let deviceID = aDecoder.decodeObject(forKey: Keys.deviceID) as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.deviceID = deviceID
|
||||||
|
displayName = aDecoder.decodeObject(forKey: Keys.displayName) as? String
|
||||||
|
lastSeenIP = aDecoder.decodeObject(forKey: Keys.lastSeenIP) as? String
|
||||||
|
lastSeenTimestamp = (aDecoder.decodeObject(forKey: Keys.lastSeenTimestamp) as? NSNumber)?.uint64Value ?? 0
|
||||||
|
lastSeenUserAgent = aDecoder.decodeObject(forKey: Keys.lastSeenUserAgent) as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
fatalError("Not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `MXUser` represents a user in Matrix.
|
||||||
|
class ClassicAppMXUser: NSObject, NSCoding {
|
||||||
|
/// The user id.
|
||||||
|
private(set) var userID: String
|
||||||
|
/// The user display name.
|
||||||
|
var displayName: String?
|
||||||
|
/// The url of the user of the avatar.
|
||||||
|
var avatarURL: String?
|
||||||
|
/// The user status.
|
||||||
|
var statusMessage: String?
|
||||||
|
|
||||||
|
/// Whether the user is currently active.
|
||||||
|
/// If YES, lastActiveAgo is an approximation and "Now" should be shown instead.
|
||||||
|
private(set) var currentlyActive = false
|
||||||
|
/// The time in milliseconds since epoch the last activity by the user has
|
||||||
|
/// been tracked by the home server.
|
||||||
|
var lastActiveLocalTimestamp: UInt64 = 0
|
||||||
|
/// Only when event.originServerTs > latestUpdateTS, we change displayname and avatarUrl.
|
||||||
|
var latestUpdateTimestamp: UInt64 = 0
|
||||||
|
|
||||||
|
// MARK: NSCoding
|
||||||
|
|
||||||
|
enum Keys {
|
||||||
|
static let userID = "userId" // String
|
||||||
|
static let displayName = "displayname" // String?
|
||||||
|
static let avatarURL = "avatarUrl" // String?
|
||||||
|
static let statusMessage = "statusMsg" // String?
|
||||||
|
static let currentlyActive = "currentlyActive" // Bool
|
||||||
|
static let lastActiveLocalTimestamp = "lastActiveLocalTS" // UInt64
|
||||||
|
static let latestUpdateTimestamp = "latestUpdateTS" // UInt64
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
guard let userID = aDecoder.decodeObject(forKey: Keys.userID) as? String else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.userID = userID
|
||||||
|
displayName = aDecoder.decodeObject(forKey: Keys.displayName) as? String
|
||||||
|
avatarURL = aDecoder.decodeObject(forKey: Keys.avatarURL) as? String
|
||||||
|
statusMessage = aDecoder.decodeObject(forKey: Keys.statusMessage) as? String
|
||||||
|
currentlyActive = aDecoder.decodeBool(forKey: Keys.currentlyActive)
|
||||||
|
// lastActiveLocalTimestamp = UInt64(aDecoder.decodeInt64(forKey: Keys.lastActiveLocalTimestamp))
|
||||||
|
// latestUpdateTimestamp = UInt64(aDecoder.decodeInt64(forKey: Keys.latestUpdateTimestamp))
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(with coder: NSCoder) {
|
||||||
|
fatalError("Not available")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2025 New Vector Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import KeychainAccess
|
||||||
|
import MatrixRustSDK
|
||||||
|
|
||||||
|
protocol ClassicAppManagerProtocol {
|
||||||
|
func loadAccounts() throws -> [ClassicAppAccount]
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClassicAppManagerError: Error {
|
||||||
|
case invalidAppGroupIdentifier(String)
|
||||||
|
case missingAccountKeys
|
||||||
|
case missingCryptoStorePassphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ClassicAppManager: ClassicAppManagerProtocol {
|
||||||
|
private enum KeychainKeys: String {
|
||||||
|
case cryptoSDKStoreKey
|
||||||
|
case accountIV = "accountIv"
|
||||||
|
case accountAESKey = "accountAesKey"
|
||||||
|
}
|
||||||
|
|
||||||
|
private let classicAppGroupIdentifier: String
|
||||||
|
private let keychain: Keychain
|
||||||
|
|
||||||
|
init(classicAppGroupIdentifier: String, classicAppKeychainServiceIdentifier: String, classicAppKeychainAccessGroupIdentifier: String) {
|
||||||
|
self.classicAppGroupIdentifier = classicAppGroupIdentifier
|
||||||
|
keychain = Keychain(service: classicAppKeychainServiceIdentifier, accessGroup: classicAppKeychainAccessGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAccounts() throws -> [ClassicAppAccount] {
|
||||||
|
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: classicAppGroupIdentifier) else {
|
||||||
|
throw ClassicAppManagerError.invalidAppGroupIdentifier(classicAppGroupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let accountIV = try keychain.getData(KeychainKeys.accountIV.rawValue),
|
||||||
|
let accountAESKey = try keychain.getData(KeychainKeys.accountAESKey.rawValue) else {
|
||||||
|
throw ClassicAppManagerError.missingAccountKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cryptoStorePassphrase = try keychain.getData(KeychainKeys.cryptoSDKStoreKey.rawValue) else {
|
||||||
|
throw ClassicAppManagerError.missingCryptoStorePassphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountManager = ClassicAppAccountManager(cacheFolder: url, iv: accountIV, aesKey: accountAESKey)
|
||||||
|
accountManager.loadAccounts()
|
||||||
|
let activeAccounts = accountManager.activeAccounts
|
||||||
|
|
||||||
|
MXLog.info("Loaded \(accountManager.accounts.count) accounts")
|
||||||
|
MXLog.verbose("Loaded accounts: \(accountManager.accounts.compactMap(\.credentials.userID).formatted(.list(type: .and)))")
|
||||||
|
MXLog.info("Found \(activeAccounts.count) active accounts")
|
||||||
|
MXLog.verbose("Active accounts: \(activeAccounts.compactMap(\.userID).formatted(.list(type: .and)))")
|
||||||
|
|
||||||
|
return activeAccounts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,12 +23,14 @@
|
|||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
|
<string>$(CLASSIC_APP_GROUP_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>keychain-access-groups</key>
|
<key>keychain-access-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
|
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -116,6 +116,12 @@
|
|||||||
<string>$(APP_GROUP_IDENTIFIER)</string>
|
<string>$(APP_GROUP_IDENTIFIER)</string>
|
||||||
<key>baseBundleIdentifier</key>
|
<key>baseBundleIdentifier</key>
|
||||||
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
|
<string>$(BASE_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>classicAppGroupIdentifier</key>
|
||||||
|
<string>$(CLASSIC_APP_GROUP_IDENTIFIER)</string>
|
||||||
|
<key>classicAppKeychainAccessGroupIdentifier</key>
|
||||||
|
<string>$(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
|
<key>classicAppKeychainServiceIdentifier</key>
|
||||||
|
<string>$(CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER)</string>
|
||||||
<key>keychainAccessGroupIdentifier</key>
|
<key>keychainAccessGroupIdentifier</key>
|
||||||
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
<string>$(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)</string>
|
||||||
<key>productionAppName</key>
|
<key>productionAppName</key>
|
||||||
|
|||||||
@@ -76,6 +76,9 @@ targets:
|
|||||||
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
|
appGroupIdentifier: $(APP_GROUP_IDENTIFIER)
|
||||||
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
|
baseBundleIdentifier: $(BASE_BUNDLE_IDENTIFIER)
|
||||||
keychainAccessGroupIdentifier: $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
keychainAccessGroupIdentifier: $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
|
classicAppGroupIdentifier: $(CLASSIC_APP_GROUP_IDENTIFIER)
|
||||||
|
classicAppKeychainServiceIdentifier: $(CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER)
|
||||||
|
classicAppKeychainAccessGroupIdentifier: $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
productionAppName: $(PRODUCTION_APP_NAME)
|
productionAppName: $(PRODUCTION_APP_NAME)
|
||||||
ITSAppUsesNonExemptEncryption: false
|
ITSAppUsesNonExemptEncryption: false
|
||||||
NSUserActivityTypes: [
|
NSUserActivityTypes: [
|
||||||
@@ -126,9 +129,11 @@ targets:
|
|||||||
com.apple.security.app-sandbox: true
|
com.apple.security.app-sandbox: true
|
||||||
com.apple.security.application-groups:
|
com.apple.security.application-groups:
|
||||||
- $(APP_GROUP_IDENTIFIER)
|
- $(APP_GROUP_IDENTIFIER)
|
||||||
|
- $(CLASSIC_APP_GROUP_IDENTIFIER)
|
||||||
com.apple.security.network.client: true
|
com.apple.security.network.client: true
|
||||||
keychain-access-groups:
|
keychain-access-groups:
|
||||||
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
- $(KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
|
- $(CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER)
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ struct AuthenticationServiceTests {
|
|||||||
|
|
||||||
service = AuthenticationService(userSessionStore: userSessionStore,
|
service = AuthenticationService(userSessionStore: userSessionStore,
|
||||||
encryptionKeyProvider: encryptionKeyProvider,
|
encryptionKeyProvider: encryptionKeyProvider,
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: clientFactory,
|
clientFactory: clientFactory,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ final class AuthenticationStartScreenViewModelTests {
|
|||||||
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
||||||
authenticationService = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
authenticationService = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: clientFactory,
|
clientFactory: clientFactory,
|
||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ struct LoginScreenViewModelTests {
|
|||||||
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
||||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: clientFactory,
|
clientFactory: clientFactory,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -359,6 +359,7 @@ final class ServerConfirmationScreenViewModelTests {
|
|||||||
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
clientFactory = AuthenticationClientFactoryMock(configuration: configuration)
|
||||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: clientFactory,
|
clientFactory: clientFactory,
|
||||||
appSettings: appSettings,
|
appSettings: appSettings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ struct ServerSelectionScreenViewModelTests {
|
|||||||
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
clientFactory = AuthenticationClientFactoryMock(configuration: .init())
|
||||||
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
service = AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
|
||||||
encryptionKeyProvider: EncryptionKeyProvider(),
|
encryptionKeyProvider: EncryptionKeyProvider(),
|
||||||
|
classicAppManager: nil,
|
||||||
clientFactory: clientFactory,
|
clientFactory: clientFactory,
|
||||||
appSettings: ServiceLocator.shared.settings,
|
appSettings: ServiceLocator.shared.settings,
|
||||||
appHooks: AppHooks())
|
appHooks: AppHooks())
|
||||||
|
|||||||
@@ -5,3 +5,8 @@ settings:
|
|||||||
BASE_BUNDLE_IDENTIFIER: io.element.elementx
|
BASE_BUNDLE_IDENTIFIER: io.element.elementx
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "colors/accent-color"
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: "colors/accent-color"
|
||||||
DEVELOPMENT_TEAM: 7J4U792NQT
|
DEVELOPMENT_TEAM: 7J4U792NQT
|
||||||
|
|
||||||
|
# Optional configuration to load accounts from Element Classic.
|
||||||
|
CLASSIC_APP_GROUP_IDENTIFIER: group.im.vector
|
||||||
|
CLASSIC_APP_KEYCHAIN_SERVICE_IDENTIFIER: im.vector.app.encryption-manager-service
|
||||||
|
CLASSIC_APP_KEYCHAIN_ACCESS_GROUP_IDENTIFIER: "$(DEVELOPMENT_TEAM).im.vector.app.keychain.shared"
|
||||||
|
|||||||
Reference in New Issue
Block a user