From 2284375fc0e16bf39009cf442a3ed563a9a474dc Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Thu, 10 Feb 2022 11:17:41 +1100 Subject: [PATCH] Started work on updated SOGS support Split the OpenGroupAPIV2 into separate files Started working on the new auth and blinded-id approaches (new auth working with un-blinded id suggesting blinded-id code is incorrect) Updated the SOGS request/response types to use Codable Updated the SOGS Request type to use enums instead of strings for keys (to reduce likelihood of typos breaking things) Updated SessionMessagingKit to use Codable and JSONEncoder/JSONDecoder instead of the legacy JSONSerialization Cleaned up some naming conventions in the SessionMessagingKit (calling a URLRequest body 'parameters' is very confusing...) Removed the custom TSRequest class (just using standard URLRequest everywhere instead) Added a number of extension functions to enable some more functional-coding styles Added extensions to Sodium methods to allow scalar multiplication and the ability to hash providing a salt and a personalisation value (both needed for new SOGS auth) Fixed an issue where the legacy auth for SOGS could crash due to threading issues (multiple threads accessing the same variable) Fixed an issue where if you were in two rooms in a single SOGS and deleted one of them, the other room would stop getting updates as the server public key was getting removed --- Session.xcodeproj/project.pbxproj | 212 +++- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- .../Open Groups/OpenGroupSuggestionGrid.swift | 6 +- .../Common Networking/Header.swift | 23 + .../Models/FileDownloadResponse.swift | 35 + .../Models/FileUploadBody.swift | 7 + .../Models/FileUploadResponse.swift | 11 + .../Common Networking/QueryParam.swift | 8 + .../File Server/FileServerAPIV2.swift | 103 +- .../File Server/Models/VersionResponse.swift | 13 + .../Jobs/NotifyPNServerJob.swift | 27 +- .../Models/AuthTokenResponse.swift | 46 + .../Open Groups/Models/CompactPollBody.swift | 27 + .../Models/CompactPollResponse.swift | 25 + .../Models/DeletedMessagesResponse.swift | 13 + .../Open Groups/Models/Deletion.swift | 23 + .../Open Groups/Models/GetInfoResponse.swift | 9 + .../Models/MemberCountResponse.swift | 13 + .../Models/ModeratorsResponse.swift | 9 + .../Models/OpenGroupMessageV2.swift | 72 ++ .../{ => Models}/OpenGroupV2.swift | 2 + .../Open Groups/Models/PublicKeyBody.swift | 13 + .../Open Groups/Models/RoomInfo.swift | 17 + .../Open Groups/Models/RoomsResponse.swift | 9 + .../Open Groups/OpenGroupAPIV2.swift | 1009 +++++++++++------ .../Open Groups/OpenGroupManagerV2.swift | 8 +- .../Open Groups/OpenGroupMessageV2.swift | 38 - .../Open Groups/Types/Endpoint.swift | 83 ++ .../Open Groups/Types/Error.swift | 25 + .../Types/NonceGenerator16Byte.swift | 9 + .../Open Groups/Types/Personalization.swift | 15 + .../Open Groups/Types/Request.swift | 57 + .../Sending & Receiving/MessageSender.swift | 18 +- .../Models/RegisterResponse.swift | 11 + .../Models/UnregisterResponse.swift | 11 + .../Notifications/PushNotificationAPI.swift | 87 +- .../Pollers/OpenGroupPollerV2.swift | 44 +- SessionMessagingKit/Utilities/Atomic.swift | 27 + .../Utilities/ECKeyPair+Conversion.swift | 34 + ...onversion.swift => Sodium+Utilities.swift} | 46 +- SessionSnodeKit/OnionRequestAPI.swift | 79 +- .../Utilities/String+Trimming.swift | 12 +- .../Crypto/ECKeyPair+Hexadecimal.swift | 6 + .../General/Array+Description.swift | 7 - .../General/Array+Utilities.swift | 23 + .../General/Data+Trimming.swift | 18 - .../General/Data+Utilities.swift | 48 + .../General/Dictionary+Description.swift | 13 - .../General/Dictionary+Utilities.swift | 42 + SessionUtilitiesKit/General/IdPrefix.swift | 12 + .../General/String+Encoding.swift | 18 + .../Meta/SessionUtilitiesKit.h | 1 - SessionUtilitiesKit/Networking/TSRequest.h | 29 - SessionUtilitiesKit/Networking/TSRequest.m | 64 -- .../MessageSender+Convenience.swift | 29 +- 55 files changed, 1927 insertions(+), 721 deletions(-) create mode 100644 SessionMessagingKit/Common Networking/Header.swift create mode 100644 SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift create mode 100644 SessionMessagingKit/Common Networking/Models/FileUploadBody.swift create mode 100644 SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift create mode 100644 SessionMessagingKit/Common Networking/QueryParam.swift create mode 100644 SessionMessagingKit/File Server/Models/VersionResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/CompactPollBody.swift create mode 100644 SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/Deletion.swift create mode 100644 SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift rename SessionMessagingKit/Open Groups/{ => Models}/OpenGroupV2.swift (97%) create mode 100644 SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift create mode 100644 SessionMessagingKit/Open Groups/Models/RoomInfo.swift create mode 100644 SessionMessagingKit/Open Groups/Models/RoomsResponse.swift delete mode 100644 SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift create mode 100644 SessionMessagingKit/Open Groups/Types/Endpoint.swift create mode 100644 SessionMessagingKit/Open Groups/Types/Error.swift create mode 100644 SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift create mode 100644 SessionMessagingKit/Open Groups/Types/Personalization.swift create mode 100644 SessionMessagingKit/Open Groups/Types/Request.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/RegisterResponse.swift create mode 100644 SessionMessagingKit/Sending & Receiving/Notifications/Models/UnregisterResponse.swift create mode 100644 SessionMessagingKit/Utilities/Atomic.swift create mode 100644 SessionMessagingKit/Utilities/ECKeyPair+Conversion.swift rename SessionMessagingKit/Utilities/{Sodium+Conversion.swift => Sodium+Utilities.swift} (55%) delete mode 100644 SessionUtilitiesKit/General/Array+Description.swift create mode 100644 SessionUtilitiesKit/General/Array+Utilities.swift delete mode 100644 SessionUtilitiesKit/General/Data+Trimming.swift create mode 100644 SessionUtilitiesKit/General/Data+Utilities.swift delete mode 100644 SessionUtilitiesKit/General/Dictionary+Description.swift create mode 100644 SessionUtilitiesKit/General/Dictionary+Utilities.swift create mode 100644 SessionUtilitiesKit/General/String+Encoding.swift delete mode 100644 SessionUtilitiesKit/Networking/TSRequest.h delete mode 100644 SessionUtilitiesKit/Networking/TSRequest.m diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 21cfe7172..b20cbbf47 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -196,7 +196,7 @@ B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */; }; B8569AD325CBA13D00DBA3DB /* MediaTextOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */; }; B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AE225CBB19A00DBA3DB /* DocumentView.swift */; }; - B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; }; + B866CE112581C1A900535CC4 /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Utilities.swift */; }; B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; }; B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; }; B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8758859264503A6000E60D0 /* JoinOpenGroupModal.swift */; }; @@ -238,7 +238,7 @@ B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; }; B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; }; B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; }; - B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; }; + B8AE75A425A6C6A6001A84D2 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */; }; B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; }; B8AE761425ABFBB9001A84D2 /* GeneralUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; }; @@ -300,7 +300,6 @@ C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */; }; C31D1DE9252172D4005D4DA8 /* ContactUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */; }; C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */; }; - C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */; }; C32824D325C9F9790062D0A7 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; }; C328250F25CA06020062D0A7 /* VoiceMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328250E25CA06020062D0A7 /* VoiceMessageView.swift */; }; C328251F25CA3A900062D0A7 /* QuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C328251E25CA3A900062D0A7 /* QuoteView.swift */; }; @@ -510,8 +509,6 @@ C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */ = {isa = PBXBuildFile; fileRef = C352A3762557859C00338F3E /* NSTimer+Proxying.h */; settings = {ATTRIBUTES = (Public, ); }; }; C352A3892557876500338F3E /* JobQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A3882557876500338F3E /* JobQueue.swift */; }; C352A3932557883D00338F3E /* JobDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A3922557883D00338F3E /* JobDelegate.swift */; }; - C352A3A62557B60D00338F3E /* TSRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = C352A3A52557B60D00338F3E /* TSRequest.m */; }; - C352A3B72557B6ED00338F3E /* TSRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = C352A3A42557B5F000338F3E /* TSRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; C3548F0624456447009433A8 /* PNModeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0524456447009433A8 /* PNModeVC.swift */; }; C3548F0824456AB6009433A8 /* UIView+Wrapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */; }; C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C354E75923FE2A7600CE22E3 /* BaseVC.swift */; }; @@ -676,7 +673,7 @@ C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; }; C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; }; C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; }; - C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; }; + C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; }; C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; }; C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; }; C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; }; @@ -684,7 +681,7 @@ C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; }; C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; }; C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; }; - C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */; }; + C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; }; C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0B42554F0E10050F1E3 /* ProofOfWork.swift */; }; C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */; }; C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -754,7 +751,6 @@ C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF1255A580500E217F9 /* OWSThumbnailService.swift */; }; C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */; }; C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; }; - C3DB6695260AC923001EFC55 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */; }; C3DB66AC260ACA42001EFC55 /* OpenGroupManagerV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */; }; C3DB66C3260ACCE6001EFC55 /* OpenGroupPollerV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66C2260ACCE6001EFC55 /* OpenGroupPollerV2.swift */; }; C3DB66CC260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */; }; @@ -778,6 +774,8 @@ FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; }; FD5D201E27B0D87C00FEA984 /* IdPrefix.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */; }; + FD5D202027B0E67900FEA984 /* String+Encoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201F27B0E67800FEA984 /* String+Encoding.swift */; }; + FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */; }; FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; }; FD705A8C278CDB5600F16121 /* SAEScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */; }; FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8D278CE29800F16121 /* String+Localization.swift */; }; @@ -787,6 +785,33 @@ FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; }; FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; + FDC4380927B31D4E00C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* Error.swift */; }; + FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380A27B31D7E00C60D73 /* Request.swift */; }; + FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */; }; + FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; + FDC4381A27B34EBA00C60D73 /* CompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */; }; + FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */; }; + FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* Endpoint.swift */; }; + FDC4382627B37F6900C60D73 /* DeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */; }; + FDC4382827B37FD300C60D73 /* ModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */; }; + FDC4382A27B3802D00C60D73 /* RoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* RoomsResponse.swift */; }; + FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */; }; + FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */; }; + FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383027B3841C00C60D73 /* RegisterResponse.swift */; }; + FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; + FDC4383A27B4696200C60D73 /* AuthTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */; }; + FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; }; + FDC4384027B4746D00C60D73 /* GetInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */; }; + FDC4384727B47F4D00C60D73 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */; }; + FDC4384827B47F4D00C60D73 /* RoomInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384427B47F4D00C60D73 /* RoomInfo.swift */; }; + FDC4384927B47F4D00C60D73 /* CompactPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */; }; + FDC4384A27B47F4D00C60D73 /* Deletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384627B47F4D00C60D73 /* Deletion.swift */; }; + FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */; }; + FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; }; + FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; }; + FDC4385727B484B700C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385627B484B700C60D73 /* FileUploadResponse.swift */; }; + FDC4385927B484E800C60D73 /* FileUploadBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385827B484E800C60D73 /* FileUploadBody.swift */; }; + FDC4385B27B485DE00C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1236,7 +1261,7 @@ B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = ""; }; B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = ""; }; B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = ""; }; - B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = ""; }; + B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = ""; }; B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneralUtilities.m; sourceTree = ""; }; B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = ""; }; @@ -1314,7 +1339,6 @@ C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = ""; }; C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = ""; }; C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = ""; }; - C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = ""; }; C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = ""; }; C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = ""; }; C328252F25CA55360062D0A7 /* ContextMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuWindow.swift; sourceTree = ""; }; @@ -1525,8 +1549,6 @@ C352A3762557859C00338F3E /* NSTimer+Proxying.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSTimer+Proxying.h"; sourceTree = ""; }; C352A3882557876500338F3E /* JobQueue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobQueue.swift; sourceTree = ""; }; C352A3922557883D00338F3E /* JobDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobDelegate.swift; sourceTree = ""; }; - C352A3A42557B5F000338F3E /* TSRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TSRequest.h; sourceTree = ""; }; - C352A3A52557B60D00338F3E /* TSRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TSRequest.m; sourceTree = ""; }; C353F8F8244809150011121A /* PNOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNOptionView.swift; sourceTree = ""; }; C3548F0524456447009433A8 /* PNModeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeVC.swift; sourceTree = ""; }; C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Wrapping.swift"; sourceTree = ""; }; @@ -1731,11 +1753,11 @@ C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Hashing.swift"; sourceTree = ""; }; C3C2A5D02553860800C340D1 /* Promise+Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Threading.swift"; sourceTree = ""; }; - C3C2A5D12553860800C340D1 /* Array+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Description.swift"; sourceTree = ""; }; + C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = ""; }; C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = ""; }; C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Delaying.swift"; sourceTree = ""; }; C3C2A5D42553860A00C340D1 /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; - C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Description.swift"; sourceTree = ""; }; + C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = ""; }; C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Retrying.swift"; sourceTree = ""; }; C3C2A5D72553860B00C340D1 /* AESGCM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AESGCM.swift; sourceTree = ""; }; C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; @@ -1767,13 +1789,12 @@ C3D9E43025676D3D0040E4F3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationMessage.swift; sourceTree = ""; }; C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = ""; }; - C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupV2.swift; sourceTree = ""; }; C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerV2.swift; sourceTree = ""; }; C3DB66C2260ACCE6001EFC55 /* OpenGroupPollerV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupPollerV2.swift; sourceTree = ""; }; C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPIV2+ObjC.swift"; sourceTree = ""; }; C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sheet.swift; sourceTree = ""; }; C3E5C2F9251DBABB0040DFFC /* EditClosedGroupVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditClosedGroupVC.swift; sourceTree = ""; }; - C3E7134E251C867C009649BB /* Sodium+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Conversion.swift"; sourceTree = ""; }; + C3E7134E251C867C009649BB /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = ""; }; C3ECBF7A257056B700EA7FCE /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = ""; }; C3F0A52F255C80BC007BE2A3 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; C3F0A5B2255C915C007BE2A3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -1813,6 +1834,8 @@ FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; FD5D201D27B0D87C00FEA984 /* IdPrefix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdPrefix.swift; sourceTree = ""; }; + FD5D201F27B0E67800FEA984 /* String+Encoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Encoding.swift"; sourceTree = ""; }; + FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ECKeyPair+Conversion.swift"; sourceTree = ""; }; FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; FD705A8B278CDB5600F16121 /* SAEScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAEScreenLockViewController.swift; sourceTree = ""; }; FD705A8D278CE29800F16121 /* String+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Localization.swift"; sourceTree = ""; }; @@ -1823,6 +1846,33 @@ FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + FDC4380827B31D4E00C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + FDC4380A27B31D7E00C60D73 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator16Byte.swift; sourceTree = ""; }; + FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; + FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactPollBody.swift; sourceTree = ""; }; + FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyBody.swift; sourceTree = ""; }; + FDC4381F27B36ADC00C60D73 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessagesResponse.swift; sourceTree = ""; }; + FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeratorsResponse.swift; sourceTree = ""; }; + FDC4382927B3802D00C60D73 /* RoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomsResponse.swift; sourceTree = ""; }; + FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberCountResponse.swift; sourceTree = ""; }; + FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisterResponse.swift; sourceTree = ""; }; + FDC4383027B3841C00C60D73 /* RegisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterResponse.swift; sourceTree = ""; }; + FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = ""; }; + FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenResponse.swift; sourceTree = ""; }; + FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; + FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetInfoResponse.swift; sourceTree = ""; }; + FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = ""; }; + FDC4384427B47F4D00C60D73 /* RoomInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomInfo.swift; sourceTree = ""; }; + FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPollResponse.swift; sourceTree = ""; }; + FDC4384627B47F4D00C60D73 /* Deletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deletion.swift; sourceTree = ""; }; + FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupV2.swift; sourceTree = ""; }; + FDC4384E27B4804F00C60D73 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = ""; }; + FDC4385027B4807400C60D73 /* QueryParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParam.swift; sourceTree = ""; }; + FDC4385627B484B700C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; + FDC4385827B484E800C60D73 /* FileUploadBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadBody.swift; sourceTree = ""; }; + FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2303,8 +2353,6 @@ B8FF8EA525C11FEF004D1F22 /* IPv4.swift */, C3C2A5D92553860B00C340D1 /* JSON.swift */, C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, - C352A3A42557B5F000338F3E /* TSRequest.h */, - C352A3A52557B60D00338F3E /* TSRequest.m */, ); path = Networking; sourceTree = ""; @@ -2330,11 +2378,11 @@ children = ( C33FDB8A255A581200E217F9 /* AppContext.h */, C33FDB85255A581100E217F9 /* AppContext.m */, - C3C2A5D12553860800C340D1 /* Array+Description.swift */, + C3C2A5D12553860800C340D1 /* Array+Utilities.swift */, C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */, B8F5F58225EC94A6003BF8D4 /* Collection+Subscripting.swift */, - B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */, - C3C2A5D52553860A00C340D1 /* Dictionary+Description.swift */, + B8AE75A325A6C6A6001A84D2 /* Data+Utilities.swift */, + C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */, B87EF18026377A1D00124B3C /* Features.swift */, B8BC00BF257D90E30032E807 /* General.swift */, C3C2A5CE2553860700C340D1 /* Logging.swift */, @@ -2357,6 +2405,7 @@ C33FDB3F255A580C00E217F9 /* String+SSK.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, FD705A8D278CE29800F16121 /* String+Localization.swift */, + FD5D201F27B0E67800FEA984 /* String+Encoding.swift */, C38EF237255B6D65007E1867 /* UIDevice+featureSupport.swift */, C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, FD705A91278D051200F16121 /* ReusableView.swift */, @@ -3043,6 +3092,7 @@ C379DC6825672B5E0002D4EB /* Notifications */ = { isa = PBXGroup; children = ( + FDC4382D27B383A600C60D73 /* Models */, C33FDB7A255A581000E217F9 /* NotificationsProtocol.h */, C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */, ); @@ -3182,11 +3232,11 @@ C3A721332558BDDF0043A11F /* Open Groups */ = { isa = PBXGroup; children = ( - C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */, + FDC4381827B34EAD00C60D73 /* Models */, + FDC4380727B31D3A00C60D73 /* Types */, B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */, C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */, C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */, - C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */, ); path = "Open Groups"; sourceTree = ""; @@ -3194,6 +3244,7 @@ C3A7215C2558C0AC0043A11F /* File Server */ = { isa = PBXGroup; children = ( + FDC4383227B385B200C60D73 /* Models */, B87EF17026367CF800124B3C /* FileServerAPIV2.swift */, ); path = "File Server"; @@ -3204,6 +3255,7 @@ children = ( C33FDB01255A580700E217F9 /* AppReadiness.h */, C33FDB75255A581000E217F9 /* AppReadiness.m */, + FDC4383D27B4708600C60D73 /* Atomic.swift */, C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */, C37F53E8255BA9BB002AEA92 /* Environment.h */, C37F5402255BA9ED002AEA92 /* Environment.m */, @@ -3241,7 +3293,8 @@ C33FDB91255A581200E217F9 /* ProtoUtils.h */, C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, - C3E7134E251C867C009649BB /* Sodium+Conversion.swift */, + C3E7134E251C867C009649BB /* Sodium+Utilities.swift */, + FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */, C33FDB31255A580A00E217F9 /* SSKEnvironment.h */, C33FDAF4255A580600E217F9 /* SSKEnvironment.m */, C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */, @@ -3329,6 +3382,7 @@ C32C5BB9256DC7C4003C73A2 /* To Do */, C3BBE0752554CDA60050F1E3 /* Configuration.swift */, C3BBE07F2554CDD70050F1E3 /* Storage.swift */, + FDC4384D27B47FD600C60D73 /* Common Networking */, B8B3201F258B1A540020074B /* Contacts */, C32C5BCB256DC818003C73A2 /* Database */, C300A5BB2554AFFB00555489 /* Messages */, @@ -3636,6 +3690,75 @@ path = Views; sourceTree = ""; }; + FDC4380727B31D3A00C60D73 /* Types */ = { + isa = PBXGroup; + children = ( + FDC4380A27B31D7E00C60D73 /* Request.swift */, + FDC4381F27B36ADC00C60D73 /* Endpoint.swift */, + FDC4380827B31D4E00C60D73 /* Error.swift */, + FDC4381627B32EC700C60D73 /* Personalization.swift */, + FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */, + ); + path = Types; + sourceTree = ""; + }; + FDC4381827B34EAD00C60D73 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */, + FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */, + FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */, + FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */, + FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */, + FDC4382927B3802D00C60D73 /* RoomsResponse.swift */, + FDC4384427B47F4D00C60D73 /* RoomInfo.swift */, + FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */, + FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */, + FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */, + FDC4384627B47F4D00C60D73 /* Deletion.swift */, + FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */, + FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FDC4382D27B383A600C60D73 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */, + FDC4383027B3841C00C60D73 /* RegisterResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FDC4383227B385B200C60D73 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4383727B3863200C60D73 /* VersionResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; + FDC4384D27B47FD600C60D73 /* Common Networking */ = { + isa = PBXGroup; + children = ( + FDC4385527B484AE00C60D73 /* Models */, + FDC4384E27B4804F00C60D73 /* Header.swift */, + FDC4385027B4807400C60D73 /* QueryParam.swift */, + ); + path = "Common Networking"; + sourceTree = ""; + }; + FDC4385527B484AE00C60D73 /* Models */ = { + isa = PBXGroup; + children = ( + FDC4385827B484E800C60D73 /* FileUploadBody.swift */, + FDC4385627B484B700C60D73 /* FileUploadResponse.swift */, + FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */, + ); + path = Models; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -3723,7 +3846,6 @@ C3D9E3FA25676BCE0040E4F3 /* TSYapDatabaseObject.h in Headers */, C3D9E3A4256763DE0040E4F3 /* AppContext.h in Headers */, C3D9E38A256760390040E4F3 /* OWSFileSystem.h in Headers */, - C352A3B72557B6ED00338F3E /* TSRequest.h in Headers */, C32C5A36256DB856003C73A2 /* LKGroupUtilities.h in Headers */, C3D9E379256760340040E4F3 /* MIMETypeUtil.h in Headers */, C3D9E50E25677A510040E4F3 /* DataSource.h in Headers */, @@ -4635,7 +4757,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */, + C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */, C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */, C3D9E41525676C320040E4F3 /* Storage.swift in Sources */, C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */, @@ -4654,17 +4776,17 @@ C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */, C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */, + FD5D202027B0E67900FEA984 /* String+Encoding.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, - C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */, + C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */, C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */, - C352A3A62557B60D00338F3E /* TSRequest.m in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, - B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */, + B8AE75A425A6C6A6001A84D2 /* Data+Utilities.swift in Sources */, B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, @@ -4699,6 +4821,7 @@ buildActionMask = 2147483647; files = ( B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, + FDC4382A27B3802D00C60D73 /* RoomsResponse.swift in Sources */, C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */, @@ -4711,6 +4834,7 @@ FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */, + FDC4384927B47F4D00C60D73 /* CompactPollResponse.swift in Sources */, C352A3932557883D00338F3E /* JobDelegate.swift in Sources */, C32C5B84256DC54F003C73A2 /* SSKEnvironment.m in Sources */, C3A3A108256E1A5C004D228D /* OWSIncomingMessageFinder.m in Sources */, @@ -4718,17 +4842,20 @@ C32C5BDD256DC88D003C73A2 /* OWSReadReceiptManager.m in Sources */, C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */, C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */, - C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */, B8B32021258B1A650020074B /* Contact.swift in Sources */, + FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */, + FDC4384027B4746D00C60D73 /* GetInfoResponse.swift in Sources */, C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */, + FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */, C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */, C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */, C352A3892557876500338F3E /* JobQueue.swift in Sources */, C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */, C32C59C1256DB41F003C73A2 /* TSGroupThread.m in Sources */, C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */, + FDC4381A27B34EBA00C60D73 /* CompactPollBody.swift in Sources */, B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */, C32C5B9F256DC739003C73A2 /* OWSBlockingManager.m in Sources */, C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */, @@ -4751,41 +4878,56 @@ B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */, C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */, + FDC4384A27B47F4D00C60D73 /* Deletion.swift in Sources */, C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */, B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */, + FDC4382827B37FD300C60D73 /* ModeratorsResponse.swift in Sources */, C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, + FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */, + FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */, + FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */, C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */, C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */, C32C5AB5256DBE8F003C73A2 /* TSOutgoingMessage+Conversion.swift in Sources */, + FDC4385927B484E800C60D73 /* FileUploadBody.swift in Sources */, C32C5E5B256DDF45003C73A2 /* OWSStorage.m in Sources */, C32C5E15256DDC78003C73A2 /* SSKPreferences.swift in Sources */, C32C5D9C256DD6DC003C73A2 /* OWSOutgoingReceiptManager.m in Sources */, B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */, C32C5C4F256DCC36003C73A2 /* Storage+OpenGroups.swift in Sources */, + FDC4384727B47F4D00C60D73 /* OpenGroupMessageV2.swift in Sources */, + FDC4384F27B4804F00C60D73 /* Header.swift in Sources */, C3DA9C0725AE7396008F7C7E /* ConfigurationMessage.swift in Sources */, B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */, FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */, + FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */, + FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */, C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */, C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */, + FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */, B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */, B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */, C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */, C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */, C32C5AAB256DBE8F003C73A2 /* TSIncomingMessage+Conversion.swift in Sources */, - B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */, + B866CE112581C1A900535CC4 /* Sodium+Utilities.swift in Sources */, C32C5A88256DBCF9003C73A2 /* MessageReceiver+Handling.swift in Sources */, C32C5C1B256DC9E0003C73A2 /* General.swift in Sources */, C32C5A02256DB658003C73A2 /* MessageSender+Convenience.swift in Sources */, B8566C6C256F60F50045A0B9 /* OWSUserProfile.m in Sources */, B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, + FDC4380927B31D4E00C60D73 /* Error.swift in Sources */, + FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */, C3DB66CC260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift in Sources */, C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */, C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */, + FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */, + FDC4382627B37F6900C60D73 /* DeletedMessagesResponse.swift in Sources */, B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */, C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */, C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */, @@ -4793,17 +4935,18 @@ B8856E94256F1C37001CE70E /* OWSSounds.m in Sources */, C32C5BCC256DC830003C73A2 /* Storage+ClosedGroups.swift in Sources */, C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */, + FDC4383A27B4696200C60D73 /* AuthTokenResponse.swift in Sources */, B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */, C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */, C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */, C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */, C32C5B1C256DC19D003C73A2 /* TSQuotedMessage.m in Sources */, - C3DB6695260AC923001EFC55 /* OpenGroupV2.swift in Sources */, C352A349255781F400338F3E /* AttachmentDownloadJob.swift in Sources */, C352A30925574D8500338F3E /* Message+Destination.swift in Sources */, C300A5E72554B07300555489 /* ExpirationTimerUpdate.swift in Sources */, B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */, C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */, + FDC4384827B47F4D00C60D73 /* RoomInfo.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, @@ -4824,16 +4967,21 @@ C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */, C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, + FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */, C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */, C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */, C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */, C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */, C32C5AF8256DC051003C73A2 /* OWSDisappearingMessagesConfiguration.m in Sources */, C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */, + FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */, C32C5B62256DC333003C73A2 /* OWSDisappearingConfigurationUpdateInfoMessage.m in Sources */, C352A2F525574B4700338F3E /* Job.swift in Sources */, + FDC4385727B484B700C60D73 /* FileUploadResponse.swift in Sources */, + FDC4385B27B485DE00C60D73 /* FileDownloadResponse.swift in Sources */, C32C5C01256DC9A0003C73A2 /* OWSIdentityManager.m in Sources */, C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */, + FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */, C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */, B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */, ); diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 866513807..2f62101f7 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -238,7 +238,7 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, return !suggestionGrid.frame.contains(location) } - func join(_ room: OpenGroupAPIV2.Info) { + func join(_ room: OpenGroupAPIV2.RoomInfo) { joinOpenGroupVC.joinV2OpenGroup(room: room.id, server: OpenGroupAPIV2.defaultServer, publicKey: OpenGroupAPIV2.defaultServerPublicKey) } diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index b62af71fc..5d0e5826a 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -3,7 +3,7 @@ import NVActivityIndicatorView final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat - private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { update() } } + private var rooms: [OpenGroupAPIV2.RoomInfo] = [] { didSet { update() } } private var heightConstraint: NSLayoutConstraint! var delegate: OpenGroupSuggestionGridDelegate? @@ -104,7 +104,7 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl extension OpenGroupSuggestionGrid { fileprivate final class Cell : UICollectionViewCell { - var room: OpenGroupAPIV2.Info? { didSet { update() } } + var room: OpenGroupAPIV2.RoomInfo? { didSet { update() } } static let identifier = "OpenGroupSuggestionGridCell" @@ -183,5 +183,5 @@ extension OpenGroupSuggestionGrid { // MARK: Delegate protocol OpenGroupSuggestionGridDelegate { - func join(_ room: OpenGroupAPIV2.Info) + func join(_ room: OpenGroupAPIV2.RoomInfo) } diff --git a/SessionMessagingKit/Common Networking/Header.swift b/SessionMessagingKit/Common Networking/Header.swift new file mode 100644 index 000000000..9081fbf05 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Header.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum Header: String { + case authorization = "Authorization" + case contentType = "Content-Type" + + case room = "Room" // TODO: Confirm this is needed + + case sogsPubKey = "X-SOGS-Pubkey" + case sogsNonce = "X-SOGS-Nonce" + case sogsTimestamp = "X-SOGS-Timestamp" + case sogsHash = "X-SOGS-Hash" +} + +// MARK: - Convenience + +extension Dictionary where Key == Header, Value == String { + func toHTTPHeaders() -> [String: String] { + return self.reduce(into: [:]) { result, next in result[next.key.rawValue] = next.value } + } +} diff --git a/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift new file mode 100644 index 000000000..b18fda763 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift @@ -0,0 +1,35 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct FileDownloadResponse: Codable { + enum CodingKeys: String, CodingKey { + case base64EncodedData = "result" + } + + let data: Data + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .base64EncodedData) + } +} + +// MARK: - Decoder + +extension FileDownloadResponse { + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) + + guard let data = Data(base64Encoded: base64EncodedData) else { + throw FileServerAPIV2.Error.parsingFailed + } + + self = FileDownloadResponse( + data: data + ) + } +} diff --git a/SessionMessagingKit/Common Networking/Models/FileUploadBody.swift b/SessionMessagingKit/Common Networking/Models/FileUploadBody.swift new file mode 100644 index 000000000..f62154b70 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Models/FileUploadBody.swift @@ -0,0 +1,7 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct FileUploadBody: Codable { + let file: String +} diff --git a/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift b/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift new file mode 100644 index 000000000..ba59e65d0 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct FileUploadResponse: Codable { + enum CodingKeys: String, CodingKey { + case fileId = "result" + } + + public let fileId: UInt64 +} diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift new file mode 100644 index 000000000..5c71ae852 --- /dev/null +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -0,0 +1,8 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum QueryParam: String { + case publicKey = "public_key" + case fromServerId = "from_server_id" +} diff --git a/SessionMessagingKit/File Server/FileServerAPIV2.swift b/SessionMessagingKit/File Server/FileServerAPIV2.swift index a5b880b36..ce5404159 100644 --- a/SessionMessagingKit/File Server/FileServerAPIV2.swift +++ b/SessionMessagingKit/File Server/FileServerAPIV2.swift @@ -22,16 +22,16 @@ public final class FileServerAPIV2 : NSObject { private override init() { } // MARK: Error - public enum Error : LocalizedError { + public enum Error: LocalizedError { case parsingFailed case invalidURL case maxFileSizeExceeded public var errorDescription: String? { switch self { - case .parsingFailed: return "Invalid response." - case .invalidURL: return "Invalid URL." - case .maxFileSizeExceeded: return "Maximum file size exceeded." + case .parsingFailed: return "Invalid response." + case .invalidURL: return "Invalid URL." + case .maxFileSizeExceeded: return "Maximum file size exceeded." } } } @@ -40,49 +40,61 @@ public final class FileServerAPIV2 : NSObject { private struct Request { let verb: HTTP.Verb let endpoint: String - let queryParameters: [String:String] - let parameters: JSON - let headers: [String:String] + let queryParameters: [QueryParam: String] + let body: Data? + let headers: [Header: String] /// Always `true` under normal circumstances. You might want to disable /// this when running over Lokinet. let useOnionRouting: Bool - init(verb: HTTP.Verb, endpoint: String, queryParameters: [String:String] = [:], parameters: JSON = [:], - headers: [String:String] = [:], useOnionRouting: Bool = true) { + init(verb: HTTP.Verb, endpoint: String, queryParameters: [QueryParam: String] = [:], body: Data? = nil, + headers: [Header: String] = [:], useOnionRouting: Bool = true) { self.verb = verb self.endpoint = endpoint self.queryParameters = queryParameters - self.parameters = parameters + self.body = body self.headers = headers self.useOnionRouting = useOnionRouting } } - // MARK: Convenience - private static func send(_ request: Request, useOldServer: Bool) -> Promise { + // MARK: - Convenience + + private static func send(_ request: Request, useOldServer: Bool) -> Promise { let server = useOldServer ? oldServer : server let serverPublicKey = useOldServer ? oldServerPublicKey : serverPublicKey - let tsRequest: TSRequest + var urlRequest: URLRequest + // TODO: Combine this 'Request' with the the pattern in OpenGroupServerV2? switch request.verb { - case .get: - var rawURL = "\(server)/\(request.endpoint)" - if !request.queryParameters.isEmpty { - let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&") - rawURL += "?\(queryString)" - } - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - tsRequest = TSRequest(url: url) - case .post, .put, .delete: - let rawURL = "\(server)/\(request.endpoint)" - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - tsRequest = TSRequest(url: url, method: request.verb.rawValue, parameters: request.parameters) + case .get: + var rawURL = "\(server)/\(request.endpoint)" + + if !request.queryParameters.isEmpty { + let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&") + rawURL += "?\(queryString)" + } + + guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } + + urlRequest = URLRequest(url: url) + + case .post, .put, .delete: + let rawURL = "\(server)/\(request.endpoint)" + + guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } + + urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.verb.rawValue + urlRequest.httpBody = request.body } - tsRequest.allHTTPHeaderFields = request.headers - if request.useOnionRouting { - return OnionRequestAPI.sendOnionRequest(tsRequest, to: server, using: serverPublicKey) - } else { + + urlRequest.allHTTPHeaderFields = request.headers.toHTTPHeaders() + + guard request.useOnionRouting else { preconditionFailure("It's currently not allowed to send non onion routed requests.") } + + return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: serverPublicKey) } // MARK: File Storage @@ -92,12 +104,17 @@ public final class FileServerAPIV2 : NSObject { } public static func upload(_ file: Data) -> Promise { - let base64EncodedFile = file.base64EncodedString() - let parameters = [ "file" : base64EncodedFile ] - let request = Request(verb: .post, endpoint: "files", parameters: parameters) - return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in - guard let fileID = json["result"] as? UInt64 else { throw Error.parsingFailed } - return fileID + let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request = Request(verb: .post, endpoint: "files", body: body) + return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in + let response: FileUploadResponse = try data.decoded(as: FileUploadResponse.self, customError: Error.parsingFailed) + + return response.fileId } } @@ -109,17 +126,21 @@ public final class FileServerAPIV2 : NSObject { public static func download(_ file: UInt64, useOldServer: Bool) -> Promise { let request = Request(verb: .get, endpoint: "files/\(file)") - return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { json in - guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed } - return file + + return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { data in + let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) + + return response.data } } public static func getVersion(_ platform: String) -> Promise { let request = Request(verb: .get, endpoint: "session_version?platform=\(platform)") - return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in - guard let version = json["result"] as? String else { throw Error.parsingFailed } - return version + + return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in + let response: VersionResponse = try data.decoded(as: VersionResponse.self, customError: Error.parsingFailed) + + return response.version } } } diff --git a/SessionMessagingKit/File Server/Models/VersionResponse.swift b/SessionMessagingKit/File Server/Models/VersionResponse.swift new file mode 100644 index 000000000..f72b4393a --- /dev/null +++ b/SessionMessagingKit/File Server/Models/VersionResponse.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension FileServerAPIV2 { + struct VersionResponse: Codable { + enum CodingKeys: String, CodingKey { + case version = "version" + } + + public let version: String + } +} diff --git a/SessionMessagingKit/Jobs/NotifyPNServerJob.swift b/SessionMessagingKit/Jobs/NotifyPNServerJob.swift index 82c57b3f3..227c66202 100644 --- a/SessionMessagingKit/Jobs/NotifyPNServerJob.swift +++ b/SessionMessagingKit/Jobs/NotifyPNServerJob.swift @@ -3,6 +3,16 @@ import SessionSnodeKit import SessionUtilitiesKit public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility + struct RequestBody: Codable { + enum CodingKeys: String, CodingKey { + case data + case sendTo = "send_to" + } + + let data: String + let sendTo: String + } + public let message: SnodeMessage public var delegate: JobDelegate? public var id: String? @@ -32,7 +42,8 @@ public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSC coder.encode(failureCount, forKey: "failureCount") } - // MARK: Running + // MARK: - Running + public func execute() { let _: Promise = execute() } @@ -42,10 +53,18 @@ public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSC JobQueue.currentlyExecutingJobs.insert(id) } let server = PushNotificationAPI.server - let parameters = [ "data" : message.data.description, "send_to" : message.recipient ] let url = URL(string: "\(server)/notify")! - let request = TSRequest(url: url, method: "POST", parameters: parameters) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + let requestBody: RequestBody = RequestBody(data: message.data.description, sendTo: message.recipient) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.httpBody = body + let promise = attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.global()) { OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: PushNotificationAPI.serverPublicKey).map { _ in } } diff --git a/SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift b/SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift new file mode 100644 index 000000000..d4340a053 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/AuthTokenResponse.swift @@ -0,0 +1,46 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct AuthTokenResponse: Codable { + struct Challenge: Codable { + enum CodingKeys: String, CodingKey { + case ciphertext = "ciphertext" + case ephemeralPublicKey = "ephemeral_public_key" + } + + let ciphertext: Data + let ephemeralPublicKey: Data + } + + let challenge: Challenge + } +} + +// MARK: - Codable + +extension OpenGroupAPIV2.AuthTokenResponse.Challenge { + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + let base64EncodedCiphertext: String = try container.decode(String.self, forKey: .ciphertext) + let base64EncodedEphemeralPublicKey: String = try container.decode(String.self, forKey: .ephemeralPublicKey) + + guard let ciphertext = Data(base64Encoded: base64EncodedCiphertext), let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else { + throw OpenGroupAPIV2.Error.parsingFailed + } + + self = OpenGroupAPIV2.AuthTokenResponse.Challenge( + ciphertext: ciphertext, + ephemeralPublicKey: ephemeralPublicKey + ) + } + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(ciphertext.base64EncodedString(), forKey: .ciphertext) + try container.encode(ephemeralPublicKey.base64EncodedString(), forKey: .ephemeralPublicKey) + } +} diff --git a/SessionMessagingKit/Open Groups/Models/CompactPollBody.swift b/SessionMessagingKit/Open Groups/Models/CompactPollBody.swift new file mode 100644 index 000000000..0e26fd773 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/CompactPollBody.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct CompactPollBody: Codable { + struct Room: Codable { + enum CodingKeys: String, CodingKey { + case id = "room_id" + case fromMessageServerId = "from_message_server_id" + case fromDeletionServerId = "from_deletion_server_id" + + // TODO: Remove this legacy value + case legacyAuthToken = "auth_token" + } + + let id: String + let fromMessageServerId: Int64? + let fromDeletionServerId: Int64? + + // TODO: This is a legacy value + let legacyAuthToken: String? + } + + let requests: [Room] + } +} diff --git a/SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift b/SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift new file mode 100644 index 000000000..626108d84 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift @@ -0,0 +1,25 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct CompactPollResponse: Codable { + public struct Result: Codable { + enum CodingKeys: String, CodingKey { + case room = "room_id" + case statusCode = "status_code" + case messages + case deletions + case moderators + } + + public let room: String + public let statusCode: UInt + public let messages: [OpenGroupMessageV2]? + public let deletions: [Deletion]? + public let moderators: [String]? + } + + public let results: [Result] + } +} diff --git a/SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift b/SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift new file mode 100644 index 000000000..5b8e34921 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/DeletedMessagesResponse.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct DeletedMessagesResponse: Codable { + enum CodingKeys: String, CodingKey { + case deletions = "ids" + } + + let deletions: [Deletion] + } +} diff --git a/SessionMessagingKit/Open Groups/Models/Deletion.swift b/SessionMessagingKit/Open Groups/Models/Deletion.swift new file mode 100644 index 000000000..d7a3e9bd9 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/Deletion.swift @@ -0,0 +1,23 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct Deletion: Codable { + enum CodingKeys: String, CodingKey { + case id + case deletedMessageID = "deleted_message_id" + } + + let id: Int64 + let deletedMessageID: Int64 + + public static func from(_ json: JSON) -> Deletion? { + guard let id = json["id"] as? Int64, let deletedMessageID = json["deleted_message_id"] as? Int64 else { + return nil + } + + return Deletion(id: id, deletedMessageID: deletedMessageID) + } + } +} diff --git a/SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift b/SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift new file mode 100644 index 000000000..6d637cddc --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct GetInfoResponse: Codable { + let room: RoomInfo + } +} diff --git a/SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift b/SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift new file mode 100644 index 000000000..2bc0ec604 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/MemberCountResponse.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct MemberCountResponse: Codable { + enum CodingKeys: String, CodingKey { + case memberCount = "member_count" + } + + let memberCount: UInt64 + } +} diff --git a/SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift b/SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift new file mode 100644 index 000000000..82c00e656 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/ModeratorsResponse.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct ModeratorsResponse: Codable { + let moderators: [String] + } +} diff --git a/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift new file mode 100644 index 000000000..76a0b11b6 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift @@ -0,0 +1,72 @@ +import Foundation +import SessionUtilitiesKit + +public struct OpenGroupMessageV2: Codable { + enum CodingKeys: String, CodingKey { + case serverID = "server_id" + case sender = "public_key" + case sentTimestamp = "timestamp" + case base64EncodedData = "data" + case base64EncodedSignature = "signature" + } + + public let serverID: Int64? + public let sender: String? + public let sentTimestamp: UInt64 + /// The serialized protobuf in base64 encoding. + public let base64EncodedData: String + /// When sending a message, the sender signs the serialized protobuf with their private key so that + /// a receiving user can verify that the message wasn't tampered with. + public let base64EncodedSignature: String? + + public func sign(with publicKey: String) -> OpenGroupMessageV2? { + // TODO: Swap to use blinded key + guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return nil } + guard let data = Data(base64Encoded: base64EncodedData) else { return nil } + guard let signature = try? Ed25519.sign(data, with: userKeyPair) else { + SNLog("Failed to sign open group message.") + return nil + } + + return OpenGroupMessageV2( + serverID: serverID, + sender: sender, + sentTimestamp: sentTimestamp, + base64EncodedData: base64EncodedData, + base64EncodedSignature: signature.base64EncodedString() + ) + } +} + +// MARK: - Decoder + +extension OpenGroupMessageV2 { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + let sender: String = try container.decode(String.self, forKey: .sender) + let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) + let base64EncodedSignature: String = try container.decode(String.self, forKey: .base64EncodedSignature) + + // Validate the message signature + guard let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { + throw OpenGroupAPIV2.Error.parsingFailed + } + + let publicKey = Data(hex: sender.removingIdPrefixIfNeeded()) + let isValid = (try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) ?? false + + guard isValid else { + SNLog("Ignoring message with invalid signature.") + throw OpenGroupAPIV2.Error.parsingFailed + } + + self = OpenGroupMessageV2( + serverID: try? container.decode(Int64.self, forKey: .serverID), + sender: sender, + sentTimestamp: try container.decode(UInt64.self, forKey: .sentTimestamp), + base64EncodedData: base64EncodedData, + base64EncodedSignature: base64EncodedSignature + ) + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupV2.swift b/SessionMessagingKit/Open Groups/Models/OpenGroupV2.swift similarity index 97% rename from SessionMessagingKit/Open Groups/OpenGroupV2.swift rename to SessionMessagingKit/Open Groups/Models/OpenGroupV2.swift index 504920b76..a95caee8d 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupV2.swift +++ b/SessionMessagingKit/Open Groups/Models/OpenGroupV2.swift @@ -1,3 +1,5 @@ +import Sodium +import SessionUtilitiesKit @objc(SNOpenGroupV2) public final class OpenGroupV2 : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility diff --git a/SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift b/SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift new file mode 100644 index 000000000..d7a5c6e24 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/PublicKeyBody.swift @@ -0,0 +1,13 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct PublicKeyBody: Codable { + enum CodingKeys: String, CodingKey { + case publicKey = "public_key" + } + + let publicKey: String + } +} diff --git a/SessionMessagingKit/Open Groups/Models/RoomInfo.swift b/SessionMessagingKit/Open Groups/Models/RoomInfo.swift new file mode 100644 index 000000000..bef83f77f --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/RoomInfo.swift @@ -0,0 +1,17 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct RoomInfo: Codable { + enum CodingKeys: String, CodingKey { + case id + case name + case imageID = "image_id" + } + + public let id: String + public let name: String + public let imageID: String? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/RoomsResponse.swift b/SessionMessagingKit/Open Groups/Models/RoomsResponse.swift new file mode 100644 index 000000000..e5ac33d23 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/RoomsResponse.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct RoomsResponse: Codable { + let rooms: [RoomInfo] + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index 0e7edc2bd..e447a6832 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -1,301 +1,462 @@ import PromiseKit import SessionSnodeKit +import Sodium +import Curve25519Kit @objc(SNOpenGroupAPIV2) -public final class OpenGroupAPIV2 : NSObject { - private static var authTokenPromises: [String:Promise] = [:] - private static var hasPerformedInitialPoll: [String:Bool] = [:] - private static var hasUpdatedLastOpenDate = false - public static let workQueue = DispatchQueue(label: "OpenGroupAPIV2.workQueue", qos: .userInitiated) // It's important that this is a serial queue - public static var moderators: [String:[String:Set]] = [:] // Server URL to room ID to set of moderator IDs - public static var defaultRoomsPromise: Promise<[Info]>? - public static var groupImagePromises: [String:Promise] = [:] - - private static let timeSinceLastOpen: TimeInterval = { - guard let lastOpen = UserDefaults.standard[.lastOpen] else { return .greatestFiniteMagnitude } - let now = Date() - return now.timeIntervalSince(lastOpen) - }() - - // MARK: Settings +public final class OpenGroupAPIV2: NSObject { + + // MARK: - Settings + public static let defaultServer = "http://116.203.70.33" public static let defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - // MARK: Error - public enum Error : LocalizedError { - case generic - case parsingFailed - case decryptionFailed - case signingFailed - case invalidURL - case noPublicKey - - public var errorDescription: String? { - switch self { - case .generic: return "An error occurred." - case .parsingFailed: return "Invalid response." - case .decryptionFailed: return "Couldn't decrypt response." - case .signingFailed: return "Couldn't sign message." - case .invalidURL: return "Invalid URL." - case .noPublicKey: return "Couldn't find server public key." - } - } - } - - // MARK: Request - private struct Request { - let verb: HTTP.Verb - let room: String? - let server: String - let endpoint: String - let queryParameters: [String:String] - let parameters: JSON - let headers: [String:String] - let isAuthRequired: Bool - /// Always `true` under normal circumstances. You might want to disable - /// this when running over Lokinet. - let useOnionRouting: Bool - - init(verb: HTTP.Verb, room: String?, server: String, endpoint: String, queryParameters: [String:String] = [:], - parameters: JSON = [:], headers: [String:String] = [:], isAuthRequired: Bool = true, useOnionRouting: Bool = true) { - self.verb = verb - self.room = room - self.server = server - self.endpoint = endpoint - self.queryParameters = queryParameters - self.parameters = parameters - self.headers = headers - self.isAuthRequired = isAuthRequired - self.useOnionRouting = useOnionRouting - } - } + // MARK: - Cache - // MARK: Info - public struct Info { - public let id: String - public let name: String - public let imageID: String? - - public init(id: String, name: String, imageID: String?) { - self.id = id - self.name = name - self.imageID = imageID - } - } - - // MARK: Compact Poll Response Body - public struct CompactPollResponseBody { - let room: String - let messages: [OpenGroupMessageV2] - let deletions: [Deletion] - let moderators: [String] - } - - public struct Deletion { - let id: Int64 - let deletedMessageID: Int64 - - public static func from(_ json: JSON) -> Deletion? { - guard let id = json["id"] as? Int64, let deletedMessageID = json["deleted_message_id"] as? Int64 else { return nil } - return Deletion(id: id, deletedMessageID: deletedMessageID) - } - } + private static var authTokenPromises: Atomic<[String: Promise]> = Atomic([:]) + private static var hasPerformedInitialPoll: [String: Bool] = [:] + private static var hasUpdatedLastOpenDate = false + public static let workQueue = DispatchQueue(label: "OpenGroupAPIV2.workQueue", qos: .userInitiated) // It's important that this is a serial queue + public static var moderators: [String: [String: Set]] = [:] // Server URL to room ID to set of moderator IDs + public static var defaultRoomsPromise: Promise<[RoomInfo]>? + public static var groupImagePromises: [String: Promise] = [:] - // MARK: Convenience - private static func send(_ request: Request) -> Promise { - let tsRequest: TSRequest - switch request.verb { - case .get: - var rawURL = "\(request.server)/\(request.endpoint)" - if !request.queryParameters.isEmpty { - let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&") - rawURL += "?\(queryString)" - } - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - tsRequest = TSRequest(url: url) - case .post, .put, .delete: - let rawURL = "\(request.server)/\(request.endpoint)" - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - tsRequest = TSRequest(url: url, method: request.verb.rawValue, parameters: request.parameters) - } - tsRequest.allHTTPHeaderFields = request.headers - tsRequest.setValue(request.room, forHTTPHeaderField: "Room") + private static let timeSinceLastOpen: TimeInterval = { + guard let lastOpen = UserDefaults.standard[.lastOpen] else { return .greatestFiniteMagnitude } + + return Date().timeIntervalSince(lastOpen) + }() + + // MARK: - Convenience + + private static func send(_ request: Request) -> Promise { + guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } + + var urlRequest: URLRequest = URLRequest(url: url) + urlRequest.httpMethod = request.verb.rawValue + urlRequest.allHTTPHeaderFields = request.headers + .setting(.room, request.room) // TODO: Is this needed anymore? Add at the request level? + .toHTTPHeaders() + urlRequest.httpBody = request.body + if request.useOnionRouting { - guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { return Promise(error: Error.noPublicKey) } - if request.isAuthRequired, let room = request.room { // Because auth happens on a per-room basis, we need both to make an authenticated request - return getAuthToken(for: room, on: request.server).then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in - tsRequest.setValue(authToken, forHTTPHeaderField: "Authorization") - let promise = OnionRequestAPI.sendOnionRequest(tsRequest, to: request.server, using: publicKey) - promise.catch(on: OpenGroupAPIV2.workQueue) { error in - // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an - // indication that the token we're using has expired. Note that a 403 has a different meaning; it means that - // we provided a valid token but it doesn't have a high enough permission level for the route in question. - if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 { - let storage = SNMessagingKitConfiguration.shared.storage - storage.writeSync { transaction in - storage.removeAuthToken(for: room, on: request.server, using: transaction) - } - } - } - return promise - } - } else { - return OnionRequestAPI.sendOnionRequest(tsRequest, to: request.server, using: publicKey) + guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { + return Promise(error: Error.noPublicKey) } - } else { - preconditionFailure("It's currently not allowed to send non onion routed requests.") + + if request.isAuthRequired { + // Determine if we should be using legacy auth for this endpoint + // TODO: Might need to store this at an OpenGroup level (so all requests can use the appropriate method) + if request.endpoint.useLegacyAuth { + // Because legacy auth happens on a per-room basis, we need to have a room to + // make an authenticated request + guard let room = request.room else { + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + } + + return getAuthToken(for: room, on: request.server) + .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in + urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue) + + let promise = OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + promise.catch(on: OpenGroupAPIV2.workQueue) { error in + // A 401 means that we didn't provide a (valid) auth token for a route + // that required one. We use this as an indication that the token we're + // using has expired. Note that a 403 has a different meaning; it means + // that we provided a valid token but it doesn't have a high enough + // permission level for the route in question. + if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 { + let storage = SNMessagingKitConfiguration.shared.storage + + storage.writeSync { transaction in + storage.removeAuthToken(for: room, on: request.server, using: transaction) + } + } + } + + return promise + } + } + + // Attempt to sign the request with the new auth + guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else { + return Promise(error: Error.signingFailed) + } + + // TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`) + return OnionRequestAPI.sendOnionRequest(signedRequest, to: request.server, using: publicKey) + } + + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) } + + preconditionFailure("It's currently not allowed to send non onion routed requests.") } - public static func compactPoll(_ server: String) -> Promise<[CompactPollResponseBody]> { - let storage = SNMessagingKitConfiguration.shared.storage - let rooms = storage.getAllV2OpenGroups().values.filter { $0.server == server }.map { $0.room } - var body: [JSON] = [] - var authTokenPromises: [String:Promise] = [:] + public static func compactPoll(_ server: String) -> Promise { + let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage + let rooms: [String] = storage.getAllV2OpenGroups().values + .filter { $0.server == server } + .map { $0.room } let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupPollerV2.maxInactivityPeriod) + hasPerformedInitialPoll[server] = true + if !hasUpdatedLastOpenDate { UserDefaults.standard[.lastOpen] = Date() hasUpdatedLastOpenDate = true } - for room in rooms { - authTokenPromises[room] = getAuthToken(for: room, on: server) - var json: JSON = [ "room_id" : room ] - if let lastMessageServerID = storage.getLastMessageServerID(for: room, on: server) { - json["from_message_server_id"] = useMessageLimit ? nil : lastMessageServerID - } - if let lastDeletionServerID = storage.getLastDeletionServerID(for: room, on: server) { - json["from_deletion_server_id"] = useMessageLimit ? nil : lastDeletionServerID - } - body.append(json) - } - return when(fulfilled: [Promise](authTokenPromises.values)).then(on: OpenGroupAPIV2.workQueue) { _ -> Promise<[CompactPollResponseBody]> in - let bodyWithAuthTokens = body.compactMap { json -> JSON? in - guard let roomID = json["room_id"] as? String, let authToken = authTokenPromises[roomID]?.value else { return nil } - var json = json - json["auth_token"] = authToken - return json - } - let request = Request(verb: .post, room: nil, server: server, endpoint: "compact_poll", parameters: [ "requests" : bodyWithAuthTokens ], isAuthRequired: false) - return send(request).then(on: OpenGroupAPIV2.workQueue) { json -> Promise<[CompactPollResponseBody]> in - guard let results = json["results"] as? [JSON] else { throw Error.parsingFailed } - let promises = results.compactMap { json -> Promise? in - guard let room = json["room_id"] as? String, let status = json["status_code"] as? UInt else { return nil } - // A 401 means that we didn't provide a (valid) auth token for a route that required one. We use this as an - // indication that the token we're using has expired. Note that a 403 has a different meaning; it means that - // we provided a valid token but it doesn't have a high enough permission level for the route in question. - guard status != 401 else { - storage.writeSync { transaction in - storage.removeAuthToken(for: room, on: server, using: transaction) - } - return nil - } - let rawDeletions = json["deletions"] as? [JSON] ?? [] - let moderators = json["moderators"] as? [String] ?? [] - return try? parseMessages(from: json, for: room, on: server).then(on: OpenGroupAPIV2.workQueue) { messages in - parseDeletions(from: rawDeletions, for: room, on: server).map(on: OpenGroupAPIV2.workQueue) { deletions in - return CompactPollResponseBody(room: room, messages: messages, deletions: deletions, moderators: moderators) - } - } + + let requestBody: CompactPollBody = CompactPollBody( + requests: rooms + .map { roomId -> CompactPollBody.Room in + CompactPollBody.Room( + id: roomId, + fromMessageServerId: (useMessageLimit ? nil : + storage.getLastMessageServerID(for: roomId, on: server) + ), + fromDeletionServerId: (useMessageLimit ? nil : + storage.getLastDeletionServerID(for: roomId, on: server) + ), + legacyAuthToken: nil + ) } - return when(fulfilled: promises) - } + ) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request = Request( + verb: .post, + room: nil, + server: server, + endpoint: .legacyCompactPoll(legacyAuth: false), + body: body, + isAuthRequired: true + ) + + return send(request) + .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in + let response: CompactPollResponse = try data.decoded(as: CompactPollResponse.self, customError: Error.parsingFailed) + + return when( + fulfilled: response.results + .map { (result: CompactPollResponse.Result) in + process(messages: result.messages, for: result.room, on: server) + .then(on: OpenGroupAPIV2.workQueue) { _ in + process(deletions: result.deletions, for: result.room, on: server) + } + } + ).then(on: OpenGroupAPIV2.workQueue) { _ in Promise.value(response) } } } - // MARK: Authorization + public static func legacyCompactPoll(_ server: String) -> Promise { + let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage + let rooms: [String] = storage.getAllV2OpenGroups().values + .filter { $0.server == server } + .map { $0.room } + var getAuthTokenPromises: [String: Promise] = [:] + let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupPollerV2.maxInactivityPeriod) + + hasPerformedInitialPoll[server] = true + + if !hasUpdatedLastOpenDate { + UserDefaults.standard[.lastOpen] = Date() + hasUpdatedLastOpenDate = true + } + + for room in rooms { + getAuthTokenPromises[room] = getAuthToken(for: room, on: server) + } + + let requestBody: CompactPollBody = CompactPollBody( + requests: rooms + .map { roomId -> CompactPollBody.Room in + CompactPollBody.Room( + id: roomId, + fromMessageServerId: (useMessageLimit ? nil : + storage.getLastMessageServerID(for: roomId, on: server) + ), + fromDeletionServerId: (useMessageLimit ? nil : + storage.getLastDeletionServerID(for: roomId, on: server) + ), + legacyAuthToken: nil + ) + } + ) + + return when(fulfilled: [Promise](getAuthTokenPromises.values)) + .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise in + let requestBodyWithAuthTokens: CompactPollBody = CompactPollBody( + requests: requestBody.requests.compactMap { oldRoom -> CompactPollBody.Room? in + guard let authToken: String = getAuthTokenPromises[oldRoom.id]?.value else { return nil } + + return CompactPollBody.Room( + id: oldRoom.id, + fromMessageServerId: oldRoom.fromMessageServerId, + fromDeletionServerId: oldRoom.fromDeletionServerId, + legacyAuthToken: authToken + ) + } + ) + + guard let body: Data = try? JSONEncoder().encode(requestBodyWithAuthTokens) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request = Request( + verb: .post, + room: nil, + server: server, + endpoint: .legacyCompactPoll(legacyAuth: true), + body: body, + isAuthRequired: false + ) + + return send(request) + .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in + let response: CompactPollResponse = try data.decoded(as: CompactPollResponse.self, customError: Error.parsingFailed) + + return when( + fulfilled: response.results + .compactMap { (result: CompactPollResponse.Result) -> Promise<[Deletion]>? in + // A 401 means that we didn't provide a (valid) auth token for a route that + // required one. We use this as an indication that the token we're using has + // expired. Note that a 403 has a different meaning; it means that we provided + // a valid token but it doesn't have a high enough permission level for the + // route in question. + guard result.statusCode != 401 else { + storage.writeSync { transaction in + storage.removeAuthToken(for: result.room, on: server, using: transaction) + } + + return nil + } + + return process(messages: result.messages, for: result.room, on: server) + .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise<[Deletion]> in + process(deletions: result.deletions, for: result.room, on: server) + } + } + ).then(on: OpenGroupAPIV2.workQueue) { _ in Promise.value(response) } + } + } + } + + // MARK: - Authentication + + // TODO: Turn 'Sodium' and 'NonceGenerator16Byte' into protocols for unit testing + static func sign( + _ request: URLRequest, + with publicKey: String, + sodium: Sodium = Sodium(), + nonceGenerator: NonceGenerator16Byte = NonceGenerator16Byte() + ) -> URLRequest? { + guard let path: String = request.url?.path else { return nil } + + var updatedRequest: URLRequest = request + let method: String = (request.httpMethod ?? "GET") + let timestamp: Int = Int(floor(Date().timeIntervalSince1970)) + let nonce: Data = Data(nonceGenerator.nonce()) + + guard let publicKeyData: Data = publicKey.dataFromHex() else { return nil } + guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { + return nil + } +// guard let blindedKeyPair: ECKeyPair = try? userKeyPair.convert(to: .blinded, with: publicKey) else { +// return nil +// } + // TODO: Change this back once you figure out why it's busted + let blindedKeyPair: ECKeyPair = userKeyPair + + // Generate the sharedSecret by "aB || A || B" where + // a, A are the users private and public keys respectively, + // B is the SOGS public key + let maybeSharedSecret: Data? = sodium.sharedSecret(blindedKeyPair.privateKey.bytes, publicKeyData.bytes)? + .appending(blindedKeyPair.publicKey) + .appending(publicKeyData.bytes) + + // Generate the hash to be sent along with the request + // intermediateHash = Blake2B(sharedSecret, size=42, salt=noncebytes, person='sogs.shared_keys') + // secretHash = Blake2B( + // Method || Path || Timestamp || Body, + // size=42, + // key=r, + // salt=noncebytes, + // person='sogs.auth_header' + // ) + let secretHashMessage: Bytes = method.bytes + .appending(path.bytes) + .appending("\(timestamp)".bytes) + .appending(request.httpBody?.bytes ?? []) // TODO: Might need to do the 'httpBodyStream' as well??? + + guard let sharedSecret: Data = maybeSharedSecret else { return nil } + guard let intermediateHash: Bytes = sodium.genericHash.hashSaltPersonal(message: sharedSecret.bytes, outputLength: 42, key: nil, salt: nonce.bytes, personal: Personalization.sharedKeys.bytes) else { + return nil + } + guard let secretHash: Bytes = sodium.genericHash.hashSaltPersonal(message: secretHashMessage, outputLength: 42, key: intermediateHash, salt: nonce.bytes, personal: Personalization.authHeader.bytes) else { + return nil + } + + updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:]) + .updated(with: [ + Header.sogsPubKey.rawValue: blindedKeyPair.hexEncodedPublicKey, + Header.sogsTimestamp.rawValue: "\(timestamp)", + Header.sogsNonce.rawValue: nonce.base64EncodedString(), + Header.sogsHash.rawValue: secretHash.toBase64() + ]) + + return updatedRequest + } + private static func getAuthToken(for room: String, on server: String) -> Promise { + // TODO: Do we need to check the `/capabilities` of the SOGS to determine if it has new auth and if not then fall back to the old auth approach?????? let storage = SNMessagingKitConfiguration.shared.storage - if let authToken = storage.getAuthToken(for: room, on: server) { + + if let authToken: String = storage.getAuthToken(for: room, on: server) { return Promise.value(authToken) - } else { - if let authTokenPromise = authTokenPromises["\(server).\(room)"] { - return authTokenPromise - } else { - let promise = requestNewAuthToken(for: room, on: server) - .then(on: OpenGroupAPIV2.workQueue) { claimAuthToken($0, for: room, on: server) } - .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in - let (promise, seal) = Promise.pending() - storage.write(with: { transaction in - storage.setAuthToken(for: room, on: server, to: authToken, using: transaction) - }, completion: { - seal.fulfill(authToken) - }) - return promise - } - promise.done(on: OpenGroupAPIV2.workQueue) { _ in - authTokenPromises["\(server).\(room)"] = nil - }.catch(on: OpenGroupAPIV2.workQueue) { _ in - authTokenPromises["\(server).\(room)"] = nil - } - authTokenPromises["\(server).\(room)"] = promise + } + + if let authTokenPromise: Promise = authTokenPromises.wrappedValue["\(server).\(room)"] { + return authTokenPromise + } + + let promise: Promise = requestNewAuthToken(for: room, on: server) + .then(on: OpenGroupAPIV2.workQueue) { claimAuthToken($0, for: room, on: server) } + .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in + let (promise, seal) = Promise.pending() + storage.write(with: { transaction in + storage.setAuthToken(for: room, on: server, to: authToken, using: transaction) + }, completion: { + seal.fulfill(authToken) + }) return promise } - } + + promise + .done(on: OpenGroupAPIV2.workQueue) { _ in + authTokenPromises.wrappedValue["\(server).\(room)"] = nil + } + .catch(on: OpenGroupAPIV2.workQueue) { _ in + authTokenPromises.wrappedValue["\(server).\(room)"] = nil + } + + authTokenPromises.wrappedValue["\(server).\(room)"] = promise + return promise } public static func requestNewAuthToken(for room: String, on server: String) -> Promise { SNLog("Requesting auth token for server: \(server).") - guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) } - let queryParameters = [ "public_key" : getUserHexEncodedPublicKey() ] - let request = Request(verb: .get, room: room, server: server, endpoint: "auth_token_challenge", queryParameters: queryParameters, isAuthRequired: false) - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let challenge = json["challenge"] as? JSON, let base64EncodedCiphertext = challenge["ciphertext"] as? String, - let base64EncodedEphemeralPublicKey = challenge["ephemeral_public_key"] as? String, let ciphertext = Data(base64Encoded: base64EncodedCiphertext), - let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else { - throw Error.parsingFailed + guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { + return Promise(error: Error.generic) + } + + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .legacyAuthTokenChallenge(legacyAuth: true), + queryParameters: [ + .publicKey: getUserHexEncodedPublicKey() + ], + isAuthRequired: false + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed) + let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) + + guard let tokenAsData = try? AESGCM.decrypt(response.challenge.ciphertext, with: symmetricKey) else { + throw Error.decryptionFailed } - let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) - guard let tokenAsData = try? AESGCM.decrypt(ciphertext, with: symmetricKey) else { throw Error.decryptionFailed } + return tokenAsData.toHexString() } } - + public static func claimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise { - let parameters = [ "public_key" : getUserHexEncodedPublicKey() ] - let headers = [ "Authorization" : authToken ] // Set explicitly here because is isn't in the database yet at this point - let request = Request(verb: .post, room: room, server: server, endpoint: "claim_auth_token", - parameters: parameters, headers: headers, isAuthRequired: false) + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request: Request = Request( + verb: .post, + room: room, + server: server, + endpoint: .legacyAuthTokenClaim(legacyAuth: true), + body: body, + headers: [ + // Set explicitly here because is isn't in the database yet at this point + .authorization: authToken + ], + isAuthRequired: false + ) + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in authToken } } - + /// Should be called when leaving a group. public static func deleteAuthToken(for room: String, on server: String) -> Promise { - let request = Request(verb: .delete, room: room, server: server, endpoint: "auth_token") + let request: Request = Request( + verb: .delete, + room: room, + server: server, + endpoint: .legacyAuthToken(legacyAuth: true) + ) + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in let storage = SNMessagingKitConfiguration.shared.storage + storage.write { transaction in storage.removeAuthToken(for: room, on: server, using: transaction) } } } - // MARK: File Storage + // MARK: - File Storage + public static func upload(_ file: Data, to room: String, on server: String) -> Promise { - let base64EncodedFile = file.base64EncodedString() - let parameters = [ "file" : base64EncodedFile ] - let request = Request(verb: .post, room: room, server: server, endpoint: "files", parameters: parameters) - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let fileID = json["result"] as? UInt64 else { throw Error.parsingFailed } - return fileID + let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request = Request(verb: .post, room: room, server: server, endpoint: .files, body: body) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response: FileUploadResponse = try data.decoded(as: FileUploadResponse.self, customError: Error.parsingFailed) + + return response.fileId } } public static func download(_ file: UInt64, from room: String, on server: String) -> Promise { - let request = Request(verb: .get, room: room, server: server, endpoint: "files/\(file)") - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed } - return file + let request = Request(verb: .get, room: room, server: server, endpoint: .file(file)) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) + + return response.data } } - // MARK: Message Sending & Receiving - public static func send(_ message: OpenGroupMessageV2, to room: String, on server: String) -> Promise { - guard let signedMessage = message.sign() else { return Promise(error: Error.signingFailed) } - guard let json = signedMessage.toJSON() else { return Promise(error: Error.parsingFailed) } - let request = Request(verb: .post, room: room, server: server, endpoint: "messages", parameters: json) - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let rawMessage = json["message"] as? JSON, let message = OpenGroupMessageV2.fromJSON(rawMessage) else { throw Error.parsingFailed } + // MARK: - Message Sending & Receiving + + public static func send(_ message: OpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { + // TODO: Test if we need a legacy version + guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) } + guard let body: Data = try? JSONEncoder().encode(signedMessage) else { + return Promise(error: Error.parsingFailed) + } + let request = Request(verb: .post, room: room, server: server, endpoint: .messages, body: body) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed) Storage.shared.write { transaction in Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction) } @@ -305,115 +466,177 @@ public final class OpenGroupAPIV2 : NSObject { public static func getMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { let storage = SNMessagingKitConfiguration.shared.storage - var queryParameters: [String:String] = [:] - if let lastMessageServerID = storage.getLastMessageServerID(for: room, on: server) { - queryParameters["from_server_id"] = String(lastMessageServerID) - } - let request = Request(verb: .get, room: room, server: server, endpoint: "messages", queryParameters: queryParameters) - return send(request).then(on: OpenGroupAPIV2.workQueue) { json -> Promise<[OpenGroupMessageV2]> in - try parseMessages(from: json, for: room, on: server) + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .messages, + queryParameters: [ + .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } + ].compactMapValues { $0 } + ) + + return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[OpenGroupMessageV2]> in + let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed) + + return process(messages: messages, for: room, on: server) } } - private static func parseMessages(from json: JSON, for room: String, on server: String) throws -> Promise<[OpenGroupMessageV2]> { + private static func process(messages: [OpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { + guard let messages: [OpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } + let storage = SNMessagingKitConfiguration.shared.storage - guard let rawMessages = json["messages"] as? [JSON] else { throw Error.parsingFailed } - let messages: [OpenGroupMessageV2] = rawMessages.compactMap { json in - guard let message = OpenGroupMessageV2.fromJSON(json), message.serverID != nil, let sender = message.sender, let data = Data(base64Encoded: message.base64EncodedData), - let base64EncodedSignature = message.base64EncodedSignature, let signature = Data(base64Encoded: base64EncodedSignature) else { - SNLog("Couldn't parse open group message from JSON: \(json).") - return nil - } - // Validate the message signature - let publicKey = Data(hex: sender.removingIdPrefixIfNeeded()) - let isValid = (try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) ?? false - guard isValid else { - SNLog("Ignoring message with invalid signature.") - return nil - } - return message - } - let serverID = messages.map { $0.serverID! }.max() ?? 0 // Safe because messages with a nil serverID are filtered out - let lastMessageServerID = storage.getLastMessageServerID(for: room, on: server) ?? 0 + let serverID: Int64 = (messages.compactMap { $0.serverID }.max() ?? 0) + let lastMessageServerID: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) + if serverID > lastMessageServerID { let (promise, seal) = Promise<[OpenGroupMessageV2]>.pending() - storage.write(with: { transaction in - storage.setLastMessageServerID(for: room, on: server, to: serverID, using: transaction) - }, completion: { - seal.fulfill(messages) - }) + + storage.write( + with: { transaction in + storage.setLastMessageServerID(for: room, on: server, to: serverID, using: transaction) + }, + completion: { + seal.fulfill(messages) + } + ) + return promise - } else { - return Promise.value(messages) } + + return Promise.value(messages) } - // MARK: Message Deletion + // MARK: - Message Deletion + public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise { - let request = Request(verb: .delete, room: room, server: server, endpoint: "messages/\(serverID)") + let request: Request = Request( + verb: .delete, + room: room, + server: server, + endpoint: .messagesForServer(serverID) + ) + // TODO: Legacy version? return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> { let storage = SNMessagingKitConfiguration.shared.storage - var queryParameters: [String:String] = [:] - if let lastDeletionServerID = storage.getLastDeletionServerID(for: room, on: server) { - queryParameters["from_server_id"] = String(lastDeletionServerID) - } - let request = Request(verb: .get, room: room, server: server, endpoint: "deleted_messages", queryParameters: queryParameters) - return send(request).then(on: OpenGroupAPIV2.workQueue) { json -> Promise<[Deletion]> in - guard let rawDeletions = json["ids"] as? [JSON] else { throw Error.parsingFailed } - return parseDeletions(from: rawDeletions, for: room, on: server) + + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .deletedMessages, + queryParameters: [ + .fromServerId: storage.getLastDeletionServerID(for: room, on: server).map { String($0) } + ].compactMapValues { $0 } + ) + // TODO: Legacy version? + return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[Deletion]> in + let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed) + + return process(deletions: response.deletions, for: room, on: server) } } - private static func parseDeletions(from rawDeletions: [JSON], for room: String, on server: String) -> Promise<[Deletion]> { + private static func process(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { + guard let deletions: [Deletion] = deletions else { return Promise.value([]) } + let storage = SNMessagingKitConfiguration.shared.storage - let deletions = rawDeletions.compactMap { Deletion.from($0) } - let serverID = deletions.map { $0.id }.max() ?? 0 - let lastDeletionServerID = storage.getLastDeletionServerID(for: room, on: server) ?? 0 + let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0) + let lastDeletionServerID: Int64 = (storage.getLastDeletionServerID(for: room, on: server) ?? 0) + if serverID > lastDeletionServerID { let (promise, seal) = Promise<[Deletion]>.pending() - storage.write(with: { transaction in - storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) - }, completion: { - seal.fulfill(deletions) - }) + + storage.write( + with: { transaction in + storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) + }, + completion: { + seal.fulfill(deletions) + } + ) + return promise - } else { - return Promise.value(deletions) } + + return Promise.value(deletions) } - // MARK: Moderation + // MARK: - Moderation + public static func getModerators(for room: String, on server: String) -> Promise<[String]> { - let request = Request(verb: .get, room: room, server: server, endpoint: "moderators") - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let moderators = json["moderators"] as? [String] else { throw Error.parsingFailed } - if var x = self.moderators[server] { - x[room] = Set(moderators) - self.moderators[server] = x - } else { - self.moderators[server] = [room:Set(moderators)] + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .moderators + ) + // TODO: Legacy version? + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed) + + if var x = self.moderators[server] { + x[room] = Set(response.moderators) + self.moderators[server] = x + } + else { + self.moderators[server] = [room: Set(response.moderators)] + } + + return response.moderators } - return moderators - } } public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise { - let parameters = [ "public_key" : publicKey ] - let request = Request(verb: .post, room: room, server: server, endpoint: "block_list", parameters: parameters) + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + // TODO: Legacy version? + let request: Request = Request( + verb: .post, + room: room, + server: server, + endpoint: .blockList, + body: body + ) + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } public static func banAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise { - let parameters = [ "public_key" : publicKey ] - let request = Request(verb: .post, room: room, server: server, endpoint: "ban_and_delete_all", parameters: parameters) + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + // TODO: Legacy version? + let request: Request = Request( + verb: .post, + room: room, + server: server, + endpoint: .banAndDeleteAll, + body: body + ) + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise { - let request = Request(verb: .delete, room: room, server: server, endpoint: "block_list/\(publicKey)") + let request: Request = Request( + verb: .delete, + room: room, + server: server, + endpoint: .blockListIndividual(publicKey) + ) + // TODO: Legacy version? return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } } @@ -421,60 +644,81 @@ public final class OpenGroupAPIV2 : NSObject { return moderators[server]?[room]?.contains(publicKey) ?? false } - // MARK: General + // MARK: - General + public static func getDefaultRoomsIfNeeded() { - Storage.shared.write(with: { transaction in - Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) - }, completion: { - let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - OpenGroupAPIV2.getAllRooms(from: defaultServer) - } - let _ = promise.done(on: OpenGroupAPIV2.workQueue) { items in - items.forEach { getGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() } - } - promise.catch(on: OpenGroupAPIV2.workQueue) { _ in - OpenGroupAPIV2.defaultRoomsPromise = nil - } - defaultRoomsPromise = promise - }) - } - - public static func getInfo(for room: String, on server: String) -> Promise { - let request = Request(verb: .get, room: room, server: server, endpoint: "rooms/\(room)", isAuthRequired: false) - let promise: Promise = send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let rawRoom = json["room"] as? JSON, let id = rawRoom["id"] as? String, let name = rawRoom["name"] as? String else { throw Error.parsingFailed } - let imageID = rawRoom["image_id"] as? String - return Info(id: id, name: name, imageID: imageID) - } - return promise - } - - public static func getAllRooms(from server: String) -> Promise<[Info]> { - let request = Request(verb: .get, room: nil, server: server, endpoint: "rooms", isAuthRequired: false) - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let rawRooms = json["rooms"] as? [JSON] else { throw Error.parsingFailed } - let rooms: [Info] = rawRooms.compactMap { json in - guard let id = json["id"] as? String, let name = json["name"] as? String else { - SNLog("Couldn't parse room from JSON: \(json).") - return nil + Storage.shared.write( + with: { transaction in + Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) + }, + completion: { + let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { + OpenGroupAPIV2.getAllRooms(from: defaultServer) } - let imageID = json["image_id"] as? String - return Info(id: id, name: name, imageID: imageID) + _ = promise.done(on: OpenGroupAPIV2.workQueue) { items in + items.forEach { getGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() } + } + promise.catch(on: OpenGroupAPIV2.workQueue) { _ in + OpenGroupAPIV2.defaultRoomsPromise = nil + } + defaultRoomsPromise = promise + } + ) + } + + public static func getInfo(for room: String, on server: String) -> Promise { + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .roomInfo(room), + isAuthRequired: false + ) + + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: GetInfoResponse = try data.decoded(as: GetInfoResponse.self, customError: Error.parsingFailed) + + return response.room + } + } + + public static func getAllRooms(from server: String) -> Promise<[RoomInfo]> { + let request: Request = Request( + verb: .get, + room: nil, + server: server, + endpoint: .rooms, + isAuthRequired: false + ) + + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: RoomsResponse = try data.decoded(as: RoomsResponse.self, customError: Error.parsingFailed) + + return response.rooms } - return rooms - } } public static func getMemberCount(for room: String, on server: String) -> Promise { - let request = Request(verb: .get, room: room, server: server, endpoint: "member_count") - return send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let memberCount = json["member_count"] as? UInt64 else { throw Error.parsingFailed } - let storage = SNMessagingKitConfiguration.shared.storage - storage.write { transaction in - storage.setUserCount(to: memberCount, forV2OpenGroupWithID: "\(server).\(room)", using: transaction) + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .legacyMemberCount(legacyAuth: true) + ) + // TODO: Non-legacy version? + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed) + + let storage = SNMessagingKitConfiguration.shared.storage + storage.write { transaction in + storage.setUserCount(to: response.memberCount, forV2OpenGroupWithID: "\(server).\(room)", using: transaction) + } + + return response.memberCount } - return memberCount - } } public static func getGroupImage(for room: String, on server: String) -> Promise { @@ -487,28 +731,41 @@ public final class OpenGroupAPIV2 : NSObject { // we only need to maintain one date in user defaults. On top of all of this we also // don't double up on fetch requests by storing the existing request as a promise if // there is one. - let lastOpenGroupImageUpdate = UserDefaults.standard[.lastOpenGroupImageUpdate] - let now = Date() - let timeSinceLastUpdate = given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude - let updateInterval: TimeInterval = 7 * 24 * 60 * 60 + let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate] + let now: Date = Date() + let timeSinceLastUpdate: TimeInterval = (given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude) + let updateInterval: TimeInterval = (7 * 24 * 60 * 60) + if let data = Storage.shared.getOpenGroupImage(for: room, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval { return Promise.value(data) - } else if let promise = groupImagePromises["\(server).\(room)"] { - return promise - } else { - let request = Request(verb: .get, room: room, server: server, endpoint: "rooms/\(room)/image", isAuthRequired: false) - let promise: Promise = send(request).map(on: OpenGroupAPIV2.workQueue) { json in - guard let base64EncodedFile = json["result"] as? String, let file = Data(base64Encoded: base64EncodedFile) else { throw Error.parsingFailed } - if server == defaultServer { - Storage.shared.write { transaction in - Storage.shared.setOpenGroupImage(to: file, for: room, on: server, using: transaction) - } - UserDefaults.standard[.lastOpenGroupImageUpdate] = now - } - return file - } - groupImagePromises["\(server).\(room)"] = promise + } + + if let promise = groupImagePromises["\(server).\(room)"] { return promise } + + let request: Request = Request( + verb: .get, + room: room, + server: server, + endpoint: .roomImage(room), + isAuthRequired: false + ) + // TODO: Legacy version (doesn't work on new SOGS) + let promise: Promise = send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) + + if server == defaultServer { + Storage.shared.write { transaction in + Storage.shared.setOpenGroupImage(to: response.data, for: room, on: server, using: transaction) + } + UserDefaults.standard[.lastOpenGroupImageUpdate] = now + } + + return response.data + } + groupImagePromises["\(server).\(room)"] = promise + + return promise } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift b/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift index 9e7f7fe73..804a1b730 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift @@ -82,6 +82,7 @@ public final class OpenGroupManagerV2 : NSObject { public func delete(_ openGroup: OpenGroupV2, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) { let storage = SNMessagingKitConfiguration.shared.storage + // Stop the poller if needed let openGroups = storage.getAllV2OpenGroups().values.filter { $0.server == openGroup.server } if openGroups.count == 1 && openGroups.last == openGroup { @@ -89,6 +90,7 @@ public final class OpenGroupManagerV2 : NSObject { poller?.stop() pollers[openGroup.server] = nil } + // Remove all data var messageIDs: Set = [] var messageTimestamps: Set = [] @@ -101,10 +103,14 @@ public final class OpenGroupManagerV2 : NSObject { Storage.shared.removeLastMessageServerID(for: openGroup.room, on: openGroup.server, using: transaction) Storage.shared.removeLastDeletionServerID(for: openGroup.room, on: openGroup.server, using: transaction) let _ = OpenGroupAPIV2.deleteAuthToken(for: openGroup.room, on: openGroup.server) - Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) thread.removeAllThreadInteractions(with: transaction) thread.remove(with: transaction) Storage.shared.removeV2OpenGroup(for: thread.uniqueId!, using: transaction) + + // Only remove the open group public key if the user isn't in any other rooms + if openGroups.count <= 1 { + Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction) + } } // MARK: Convenience diff --git a/SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift deleted file mode 100644 index bc82ad7ce..000000000 --- a/SessionMessagingKit/Open Groups/OpenGroupMessageV2.swift +++ /dev/null @@ -1,38 +0,0 @@ - -public struct OpenGroupMessageV2 { - public let serverID: Int64? - public let sender: String? - public let sentTimestamp: UInt64 - /// The serialized protobuf in base64 encoding. - public let base64EncodedData: String - /// When sending a message, the sender signs the serialized protobuf with their private key so that - /// a receiving user can verify that the message wasn't tampered with. - public let base64EncodedSignature: String? - - public func sign() -> OpenGroupMessageV2? { - let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()! - let data = Data(base64Encoded: base64EncodedData)! - guard let signature = try? Ed25519.sign(data, with: userKeyPair) else { - SNLog("Failed to sign open group message.") - return nil - } - return OpenGroupMessageV2(serverID: serverID, sender: sender, sentTimestamp: sentTimestamp, - base64EncodedData: base64EncodedData, base64EncodedSignature: signature.base64EncodedString()) - } - - public func toJSON() -> JSON? { - var result: JSON = [ "data" : base64EncodedData, "timestamp" : sentTimestamp ] - if let serverID = serverID { result["server_id"] = serverID } - if let sender = sender { result["public_key"] = sender } - if let base64EncodedSignature = base64EncodedSignature { result["signature"] = base64EncodedSignature } - return result - } - - public static func fromJSON(_ json: JSON) -> OpenGroupMessageV2? { - guard let base64EncodedData = json["data"] as? String, let sentTimestamp = json["timestamp"] as? UInt64 else { return nil } - let serverID = json["server_id"] as? Int64 - let sender = json["public_key"] as? String - let base64EncodedSignature = json["signature"] as? String - return OpenGroupMessageV2(serverID: serverID, sender: sender, sentTimestamp: sentTimestamp, base64EncodedData: base64EncodedData, base64EncodedSignature: base64EncodedSignature) - } -} diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/Endpoint.swift new file mode 100644 index 000000000..ac6b4526b --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/Endpoint.swift @@ -0,0 +1,83 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum Endpoint { + case files + case file(UInt64) + + case messages + case messagesForServer(Int64) + case deletedMessages + + case moderators + + case blockList + case blockListIndividual(String) + case banAndDeleteAll + + case rooms + case roomInfo(String) + case roomImage(String) + + // Legacy endpoints (to be deprecated and removed) + case legacyCompactPoll(legacyAuth: Bool) + case legacyAuthToken(legacyAuth: Bool) + case legacyAuthTokenChallenge(legacyAuth: Bool) + case legacyAuthTokenClaim(legacyAuth: Bool) + case legacyMemberCount(legacyAuth: Bool) + + var path: String { + switch self { + case .files: return "files" + case .file(let fileId): return "files/\(fileId)" + + case .messages: return "messages" + case .messagesForServer(let serverId): return "messages/\(serverId)" + case .deletedMessages: return "deleted_messages" + + case .moderators: return "moderators" + + case .blockList: return "block_list" + case .blockListIndividual(let publicKey): return "block_list/\(publicKey)" + case .banAndDeleteAll: return "ban_and_delete_all" + + case .rooms: return "rooms" + case .roomInfo(let roomName): return "rooms/\(roomName)" + case .roomImage(let roomName): return "rooms/\(roomName)/image" + + // Legacy endpoints (to be deprecated and removed) + // TODO: Look for a nicer way to prepend 'legacy'? (OnionRequestAPI messes with this but the new auth needs it to be correct...) + case .legacyCompactPoll(let useLegacyAuth): + return "\(useLegacyAuth ? "" : "legacy/")compact_poll" + + case .legacyAuthToken(let useLegacyAuth): + return "\(useLegacyAuth ? "" : "legacy/")auth_token" + + case .legacyAuthTokenChallenge(let useLegacyAuth): + return "\(useLegacyAuth ? "" : "legacy/")auth_token_challenge" + + case .legacyAuthTokenClaim(let useLegacyAuth): + return "\(useLegacyAuth ? "" : "legacy/")claim_auth_token" + + case .legacyMemberCount(let useLegacyAuth): + return "\(useLegacyAuth ? "" : "legacy/")member_count" + } + } + + var useLegacyAuth: Bool { + switch self { + // File upload/download should use legacy auth + case .files, .file: return true + + case .legacyCompactPoll(let useLegacyAuth), + .legacyAuthToken(let useLegacyAuth), + .legacyAuthTokenChallenge(let useLegacyAuth), + .legacyAuthTokenClaim(let useLegacyAuth), + .legacyMemberCount(let useLegacyAuth): + return useLegacyAuth + + default: return false + } + } +} diff --git a/SessionMessagingKit/Open Groups/Types/Error.swift b/SessionMessagingKit/Open Groups/Types/Error.swift new file mode 100644 index 000000000..52610469f --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/Error.swift @@ -0,0 +1,25 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public enum Error: LocalizedError { + case generic + case parsingFailed + case decryptionFailed + case signingFailed + case invalidURL + case noPublicKey + + public var errorDescription: String? { + switch self { + case .generic: return "An error occurred." + case .parsingFailed: return "Invalid response." + case .decryptionFailed: return "Couldn't decrypt response." + case .signingFailed: return "Couldn't sign message." + case .invalidURL: return "Invalid URL." + case .noPublicKey: return "Couldn't find server public key." + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift b/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift new file mode 100644 index 000000000..e62a1c974 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/NonceGenerator16Byte.swift @@ -0,0 +1,9 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Sodium + +extension OpenGroupAPIV2 { + class NonceGenerator16Byte: NonceGenerator { + var NonceBytes: Int { 16 } + } +} diff --git a/SessionMessagingKit/Open Groups/Types/Personalization.swift b/SessionMessagingKit/Open Groups/Types/Personalization.swift new file mode 100644 index 000000000..44235af0d --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/Personalization.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Sodium + +extension OpenGroupAPIV2 { + public enum Personalization: String { + case sharedKeys = "sogs.shared_keys" + case authHeader = "sogs.auth_header" + + var bytes: Bytes { + return self.rawValue.bytes + } + } +} diff --git a/SessionMessagingKit/Open Groups/Types/Request.swift b/SessionMessagingKit/Open Groups/Types/Request.swift new file mode 100644 index 000000000..cb66217c3 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Types/Request.swift @@ -0,0 +1,57 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + struct Request { + let verb: HTTP.Verb + let room: String? + let server: String + let endpoint: Endpoint + let queryParameters: [QueryParam: String] + let body: Data? + let headers: [Header: String] + let isAuthRequired: Bool + /// Always `true` under normal circumstances. You might want to disable + /// this when running over Lokinet. + let useOnionRouting: Bool + + init( + verb: HTTP.Verb, + room: String?, + server: String, + endpoint: Endpoint, + queryParameters: [QueryParam: String] = [:], + body: Data? = nil, + headers: [Header: String] = [:], + isAuthRequired: Bool = true, + useOnionRouting: Bool = true + ) { + self.verb = verb + self.room = room + self.server = server + self.endpoint = endpoint + self.queryParameters = queryParameters + self.body = body + self.headers = headers + self.isAuthRequired = isAuthRequired + self.useOnionRouting = useOnionRouting + } + + var url: URL? { + guard verb == .get else { return URL(string: "\(server)/\(endpoint.path)") } + + return URL( + string: [ + "\(server)/\(endpoint.path)", + queryParameters + .map { key, value in "\(key.rawValue)=\(value)" } + .joined(separator: "&") + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: "?") + ) + } + } +} diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index fcb4f1a54..8542ea923 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -257,7 +257,8 @@ public final class MessageSender : NSObject { return promise } - // MARK: Open Groups + // MARK: - Open Groups + internal static func sendToOpenGroupDestination(_ destination: Message.Destination, message: Message, using transaction: Any) -> Promise { let (promise, seal) = Promise.pending() let storage = SNMessagingKitConfiguration.shared.storage @@ -266,7 +267,15 @@ public final class MessageSender : NSObject { if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set message.sentTimestamp = NSDate.millisecondTimestamp() } - message.sender = storage.getUserPublicKey() + + guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { + preconditionFailure() + } + + if let userDerivedKey: ECKeyPair = try? OWSIdentityManager.shared().identityKeyPair()?.convert(to: .blinded, with: openGroupV2.publicKey) { + message.sender = userDerivedKey.hexEncodedPublicKey + } + switch destination { case .contact(_): preconditionFailure() case .closedGroup(_): preconditionFailure() @@ -308,9 +317,12 @@ public final class MessageSender : NSObject { } // Send the result guard case .openGroupV2(let room, let server) = destination else { preconditionFailure() } + // TODO: Determine if the 'getV2OpenGroup' call will cause issues + guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { preconditionFailure() } let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!, base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil) - OpenGroupAPIV2.send(openGroupMessage, to: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in + + OpenGroupAPIV2.send(openGroupMessage, to: room, on: server, with: openGroupV2.publicKey).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) } storage.write(with: { transaction in MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: openGroupMessage.sentTimestamp, using: transaction) diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/RegisterResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/RegisterResponse.swift new file mode 100644 index 000000000..c1add2d2d --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/RegisterResponse.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct RegisterResponse: Codable { + let body: String + let code: Int + let message: String? + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnregisterResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnregisterResponse.swift new file mode 100644 index 000000000..d14776e76 --- /dev/null +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/UnregisterResponse.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension PushNotificationAPI { + struct UnregisterResponse: Codable { + let body: String + let code: Int + let message: String? + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 8fccb96ec..a9f5b8d02 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -3,10 +3,20 @@ import PromiseKit @objc(LKPushNotificationAPI) public final class PushNotificationAPI : NSObject { + struct RequestBody: Codable { + let token: String + let pubKey: String? + } + + struct ClosedGroupRequestBody: Codable { + let token: String + let pubKey: String + } - // MARK: Settings + // MARK: - Settings public static let server = "https://live.apns.getsession.org" public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049" + private static let maxRetryCount: UInt = 4 private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60 @@ -15,29 +25,38 @@ public final class PushNotificationAPI : NSObject { public var endpoint: String { switch self { - case .subscribe: return "subscribe_closed_group" - case .unsubscribe: return "unsubscribe_closed_group" + case .subscribe: return "subscribe_closed_group" + case .unsubscribe: return "unsubscribe_closed_group" } } } - // MARK: Initialization + // MARK: - Initialization + private override init() { } - // MARK: Registration + // MARK: - Registration + public static func unregister(_ token: Data) -> Promise { - let hexEncodedToken = token.toHexString() - let parameters = [ "token" : hexEncodedToken ] + let requestBody: RequestBody = RequestBody(token: token.toHexString(), pubKey: nil) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + let url = URL(string: "\(server)/unregister")! - let request = TSRequest(url: url, method: "POST", parameters: parameters) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.httpBody = body + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { + guard let response: UnregisterResponse = try? response.decoded(as: UnregisterResponse.self) else { return SNLog("Couldn't unregister from push notifications.") } - guard json["code"] as? Int != 0 else { - return SNLog("Couldn't unregister from push notifications due to error: \(json["message"] as? String ?? "nil").") + guard response.code != 0 else { + return SNLog("Couldn't unregister from push notifications due to error: \(response.message ?? "nil").") } } } @@ -57,7 +76,13 @@ public final class PushNotificationAPI : NSObject { } public static func register(with token: Data, publicKey: String, isForcedUpdate: Bool) -> Promise { - let hexEncodedToken = token.toHexString() + let hexEncodedToken: String = token.toHexString() + let requestBody: RequestBody = RequestBody(token: hexEncodedToken, pubKey: publicKey) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + let userDefaults = UserDefaults.standard let oldToken = userDefaults[.deviceToken] let lastUploadTime = userDefaults[.lastDeviceTokenUpload] @@ -66,18 +91,22 @@ public final class PushNotificationAPI : NSObject { SNLog("Device token hasn't changed or expired; no need to re-upload.") return Promise { $0.fulfill(()) } } - let parameters = [ "token" : hexEncodedToken, "pubKey" : publicKey ] + let url = URL(string: "\(server)/register")! - let request = TSRequest(url: url, method: "POST", parameters: parameters) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.httpBody = body + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { + guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else { return SNLog("Couldn't register device token.") } - guard json["code"] as? Int != 0 else { - return SNLog("Couldn't register device token due to error: \(json["message"] as? String ?? "nil").") + guard response.code != 0 else { + return SNLog("Couldn't register device token due to error: \(response.message ?? "nil").") } + userDefaults[.deviceToken] = hexEncodedToken userDefaults[.lastDeviceTokenUpload] = now userDefaults[.isUsingFullAPNs] = true @@ -101,18 +130,26 @@ public final class PushNotificationAPI : NSObject { @discardableResult public static func performOperation(_ operation: ClosedGroupOperation, for closedGroupPublicKey: String, publicKey: String) -> Promise { let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs] + let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody(token: closedGroupPublicKey, pubKey: publicKey) + guard isUsingFullAPNs else { return Promise { $0.fulfill(()) } } - let parameters = [ "closedGroupPublicKey" : closedGroupPublicKey, "pubKey" : publicKey ] + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + let url = URL(string: "\(server)/\(operation.endpoint)")! - let request = TSRequest(url: url, method: "POST", parameters: parameters) - request.allHTTPHeaderFields = [ "Content-Type" : "application/json" ] + var request: URLRequest = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = [ Header.contentType.rawValue: "application/json" ] + request.httpBody = body + let promise: Promise = attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global()) { OnionRequestAPI.sendOnionRequest(request, to: server, target: "/loki/v2/lsrpc", using: serverPublicKey).map2 { response in - guard let json = response["body"] as? JSON else { + guard let response: RegisterResponse = try? response.decoded(as: RegisterResponse.self) else { return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).") } - guard json["code"] as? Int != 0 else { - return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(json["message"] as? String ?? "nil").") + guard response.code != 0 else { + return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.message ?? "nil").") } } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift index d5eee9ad1..549a8dda0 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPollerV2.swift @@ -46,25 +46,32 @@ public final class OpenGroupPollerV2 : NSObject { self.isPolling = true let (promise, seal) = Promise.pending() promise.retainUntilComplete() - OpenGroupAPIV2.compactPoll(server).done(on: OpenGroupAPIV2.workQueue) { [weak self] bodies in - guard let self = self else { return } - self.isPolling = false - bodies.forEach { self.handleCompactPollBody($0, isBackgroundPoll: isBackgroundPoll) } - seal.fulfill(()) - }.catch(on: OpenGroupAPIV2.workQueue) { error in - SNLog("Open group polling failed due to error: \(error).") - self.isPolling = false - seal.fulfill(()) // The promise is just used to keep track of when we're done - } + + // TODO: Update to use the non-legacy version +// OpenGroupAPIV2.compactPoll(server) + OpenGroupAPIV2.legacyCompactPoll(server) + .done(on: OpenGroupAPIV2.workQueue) { [weak self] response in + guard let self = self else { return } + self.isPolling = false + response.results.forEach { self.handleCompactPollBody($0, isBackgroundPoll: isBackgroundPoll) } + seal.fulfill(()) + } + .catch(on: OpenGroupAPIV2.workQueue) { error in + SNLog("Open group polling failed due to error: \(error).") + self.isPolling = false + seal.fulfill(()) // The promise is just used to keep track of when we're done + } + return promise } - private func handleCompactPollBody(_ body: OpenGroupAPIV2.CompactPollResponseBody, isBackgroundPoll: Bool) { + private func handleCompactPollBody(_ body: OpenGroupAPIV2.CompactPollResponse.Result, isBackgroundPoll: Bool) { let storage = SNMessagingKitConfiguration.shared.storage // - Messages // Sorting the messages by server ID before importing them fixes an issue where messages that quote older messages can't find those older messages let openGroupID = "\(server).\(body.room)" - let messages = body.messages.sorted { $0.serverID! < $1.serverID! } // Safe because messages with a nil serverID are filtered out + let messages = (body.messages ?? []).sorted { ($0.serverID ?? 0) < ($1.serverID ?? 0) } + storage.write { transaction in messages.forEach { message in guard let data = Data(base64Encoded: message.base64EncodedData) else { @@ -82,24 +89,29 @@ public final class OpenGroupPollerV2 : NSObject { } } } + // - Moderators if var x = OpenGroupAPIV2.moderators[server] { - x[body.room] = Set(body.moderators) + x[body.room] = Set(body.moderators ?? []) OpenGroupAPIV2.moderators[server] = x - } else { - OpenGroupAPIV2.moderators[server] = [ body.room : Set(body.moderators) ] } + else { + OpenGroupAPIV2.moderators[server] = [ body.room : Set(body.moderators ?? []) ] + } + // - Deletions - let deletedMessageServerIDs = Set(body.deletions.map { UInt64($0.deletedMessageID) }) + let deletedMessageServerIDs = Set((body.deletions ?? []).map { UInt64($0.deletedMessageID) }) storage.write { transaction in let transaction = transaction as! YapDatabaseReadWriteTransaction guard let threadID = storage.v2GetThreadID(for: openGroupID), let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { return } var messagesToRemove: [TSMessage] = [] + thread.enumerateInteractions(with: transaction) { interaction, stop in guard let message = interaction as? TSMessage, deletedMessageServerIDs.contains(message.openGroupServerMessageID) else { return } messagesToRemove.append(message) } + messagesToRemove.forEach { $0.remove(with: transaction) } } } diff --git a/SessionMessagingKit/Utilities/Atomic.swift b/SessionMessagingKit/Utilities/Atomic.swift new file mode 100644 index 000000000..8ba7ca568 --- /dev/null +++ b/SessionMessagingKit/Utilities/Atomic.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +/// The `Atomic` wrapper is a generic wrapper providing a thread-safe way to get and set a value +@propertyWrapper +struct Atomic { + private let lock = DispatchSemaphore(value: 1) + private var value: Value + + init(_ initialValue: Value) { + self.value = initialValue + } + + var wrappedValue: Value { + get { + lock.wait() + defer { lock.signal() } + return value + } + set { + lock.wait() + value = newValue + lock.signal() + } + } +} diff --git a/SessionMessagingKit/Utilities/ECKeyPair+Conversion.swift b/SessionMessagingKit/Utilities/ECKeyPair+Conversion.swift new file mode 100644 index 000000000..f420b5164 --- /dev/null +++ b/SessionMessagingKit/Utilities/ECKeyPair+Conversion.swift @@ -0,0 +1,34 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import Curve25519Kit +import SessionUtilitiesKit +import Sodium + +public extension ECKeyPair { + func convert(to targetPrefix: IdPrefix, with otherKey: String, using sodium: Sodium = Sodium()) throws -> ECKeyPair? { + guard let publicKeyPrefix: IdPrefix = IdPrefix(with: hexEncodedPublicKey) else { return nil } + + switch (publicKeyPrefix, targetPrefix) { + case (.standard, .blinded): // Only support standard -> blinded conversions + // TODO: Figure out why this is broken... +// guard let otherPubKeyData: Data = otherKey.data(using: .utf8) else { return nil } + guard let otherPubKeyData: Data = otherKey.dataFromHex() else { return nil } + guard let otherPubKeyHashBytes: Bytes = sodium.genericHash.hash(message: [UInt8](otherPubKeyData)) else { + return nil + } + guard let blindedPublicKey: Sodium.SharedSecret = sodium.sharedSecret(otherPubKeyHashBytes, [UInt8](publicKey)) else { + return nil + } + guard let blindedPrivateKey: Sodium.SharedSecret = sodium.sharedSecret(otherPubKeyHashBytes, [UInt8](privateKey)) else { + return nil + } + + return try BlindedECKeyPair(publicKeyData: blindedPublicKey, privateKeyData: blindedPrivateKey) + + case (.standard, .standard): return self + case (.blinded, .blinded): return self + default: return nil + } + } +} diff --git a/SessionMessagingKit/Utilities/Sodium+Conversion.swift b/SessionMessagingKit/Utilities/Sodium+Utilities.swift similarity index 55% rename from SessionMessagingKit/Utilities/Sodium+Conversion.swift rename to SessionMessagingKit/Utilities/Sodium+Utilities.swift index 9ad03dc67..d71891fd3 100644 --- a/SessionMessagingKit/Utilities/Sodium+Conversion.swift +++ b/SessionMessagingKit/Utilities/Sodium+Utilities.swift @@ -41,22 +41,27 @@ extension Sign { } extension Sodium { - public typealias SOGSDerivedKey = Data + public typealias SharedSecret = Data private static let publicKeyBytes: Int = Int(crypto_scalarmult_bytes()) private static let sharedSecretBytes: Int = Int(crypto_scalarmult_bytes()) - public func derivedKey(serverPublicKeyBytes: [UInt8], userKeyBytes: [UInt8]) -> SOGSDerivedKey? { - guard serverPublicKeyBytes.count == Sodium.publicKeyBytes && userKeyBytes.count == Sodium.publicKeyBytes else { return nil } + public func sharedSecret(_ firstKeyBytes: [UInt8], _ secondKeyBytes: [UInt8]) -> SharedSecret? { + guard firstKeyBytes.count == Sodium.publicKeyBytes && secondKeyBytes.count == Sodium.publicKeyBytes else { + return nil + } let sharedSecretPtr: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: Sodium.sharedSecretBytes) - let result = userKeyBytes.withUnsafeBytes { (userPublicKeyPtr: UnsafeRawBufferPointer) in - return serverPublicKeyBytes.withUnsafeBytes { (serverPublicKeyPtr: UnsafeRawBufferPointer) -> Int32 in - guard let serverKeyBaseAddress: UnsafePointer = serverPublicKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self), let userKeyBaseAddress: UnsafePointer = userPublicKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + let result = secondKeyBytes.withUnsafeBytes { (secondKeyPtr: UnsafeRawBufferPointer) -> Int32 in + return firstKeyBytes.withUnsafeBytes { (firstKeyPtr: UnsafeRawBufferPointer) -> Int32 in + guard let firstKeyBaseAddress: UnsafePointer = firstKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { + return -1 + } + guard let secondKeyBaseAddress: UnsafePointer = secondKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return -1 } - return crypto_scalarmult(sharedSecretPtr, serverKeyBaseAddress, userKeyBaseAddress) + return crypto_scalarmult(sharedSecretPtr, firstKeyBaseAddress, secondKeyBaseAddress) } } @@ -65,3 +70,30 @@ extension Sodium { return Data(bytes: sharedSecretPtr, count: Sodium.sharedSecretBytes) } } + +extension GenericHash { + public func hashSaltPersonal( + message: Bytes, + outputLength: Int, + key: Bytes? = nil, + salt: Bytes, + personal: Bytes + ) -> Bytes? { + var output: [UInt8] = [UInt8](repeating: 0, count: outputLength) + + let result = crypto_generichash_blake2b_salt_personal( + &output, + outputLength, + message, + UInt64(message.count), + key, + (key?.count ?? 0), + salt, + personal + ) + + guard result == 0 else { return nil } + + return output + } +} diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 61e617c72..e6f89cb85 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -1,3 +1,4 @@ +import Foundation import CryptoSwift import PromiseKit import SessionUtilitiesKit @@ -301,54 +302,54 @@ public enum OnionRequestAPI { // MARK: Public API /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { + public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] - return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in + return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error } throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error } } /// Sends an onion request to `server`. Builds new paths as needed. - public static func sendOnionRequest(_ request: NSURLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise { - var rawHeaders = request.allHTTPHeaderFields ?? [:] - rawHeaders.removeValue(forKey: "User-Agent") - var headers: JSON = rawHeaders.mapValues { value in - switch value.lowercased() { - case "true": return true - case "false": return false - default: return value - } - } + public static func sendOnionRequest(_ request: URLRequest, to server: String, target: String = "/loki/v3/lsrpc", using x25519PublicKey: String) -> Promise { guard let url = request.url, let host = request.url?.host else { return Promise(error: Error.invalidURL) } - var endpoint = url.path.removingPrefix("/") - if let query = url.query { endpoint += "?\(query)" } - let scheme = url.scheme - let port = given(url.port) { UInt16($0) } - let parametersAsString: String - if let tsRequest = request as? TSRequest { - headers["Content-Type"] = "application/json" - let tsRequestParameters = tsRequest.parameters - if !tsRequestParameters.isEmpty { - guard let parameters = try? JSONSerialization.data(withJSONObject: tsRequestParameters, options: [ .fragmentsAllowed ]) else { - return Promise(error: HTTP.Error.invalidJSON) + + var headers: JSON = (request.allHTTPHeaderFields ?? [:]) + .mapValues { value -> Any in + switch value.lowercased() { + case "true": return true + case "false": return false + default: return value } - parametersAsString = String(bytes: parameters, encoding: .utf8) ?? "null" - } else { - parametersAsString = "null" - } - } else { - headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] - if let parametersAsInputStream = request.httpBodyStream, let parameters = try? Data(from: parametersAsInputStream) { - parametersAsString = "{ \"fileUpload\" : \"\(String(data: parameters.base64EncodedData(), encoding: .utf8) ?? "null")\" }" - } else { - parametersAsString = "null" } + .removingValue(forKey: "User-Agent") + + // Note: We need to remove the leading forward slash unless we are explicitly hitting a legacy + // endpoint (in which case we need it to ensure the request signing works correctly + // TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints + let endpoint: String = url.path + .removingPrefix("/", if: !url.path.starts(with: "/legacy")) + .appending(url.query.map { value in "?\(value)" }) + let scheme: String? = url.scheme + let port: UInt16? = url.port.map { UInt16($0) } + let bodyAsString: String + + if let body: Data = request.httpBody { + headers["Content-Type"] = "application/json" // Assume data is JSON + bodyAsString = (String(data: body, encoding: .utf8) ?? "null") } + else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) { + headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] + bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" + } + else { + bodyAsString = "null" + } + let payload: JSON = [ - "body" : parametersAsString, + "body" : bodyAsString, "endpoint" : endpoint, - "method" : request.httpMethod!, + "method" : (request.httpMethod ?? "GET"), // Default (if nil) is 'GET' "headers" : headers ] let destination = Destination.server(host: host, target: target, x25519PublicKey: x25519PublicKey, scheme: scheme, port: port) @@ -359,8 +360,8 @@ public enum OnionRequestAPI { return promise } - public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise { - let (promise, seal) = Promise.pending() + public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise { + let (promise, seal) = Promise.pending() var guardSnode: Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` buildOnion(around: payload, targetedAt: destination).done2 { intermediate in @@ -401,12 +402,12 @@ public enum OnionRequestAPI { guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) } - seal.fulfill(body) + seal.fulfill(data) } else { guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) } - seal.fulfill(json) + seal.fulfill(data) } } catch { seal.reject(error) diff --git a/SessionSnodeKit/Utilities/String+Trimming.swift b/SessionSnodeKit/Utilities/String+Trimming.swift index 6d412b450..5e1f743e6 100644 --- a/SessionSnodeKit/Utilities/String+Trimming.swift +++ b/SessionSnodeKit/Utilities/String+Trimming.swift @@ -2,8 +2,18 @@ import Foundation internal extension String { - func removingPrefix(_ prefix: String) -> String { + func removingPrefix(_ prefix: String, if condition: Bool = true) -> String { + guard condition else { return self } guard let range = self.range(of: prefix), range.lowerBound == startIndex else { return self } + return String(self[range.upperBound.. String { + guard let value: String = other else { return self } + + return self.appending(value) + } +} diff --git a/SessionUtilitiesKit/Crypto/ECKeyPair+Hexadecimal.swift b/SessionUtilitiesKit/Crypto/ECKeyPair+Hexadecimal.swift index 72f15c5de..c1ac78934 100644 --- a/SessionUtilitiesKit/Crypto/ECKeyPair+Hexadecimal.swift +++ b/SessionUtilitiesKit/Crypto/ECKeyPair+Hexadecimal.swift @@ -20,3 +20,9 @@ public extension ECKeyPair { return true } } + +public extension BlindedECKeyPair { + @objc override var hexEncodedPublicKey: String { + return IdPrefix.blinded.rawValue + publicKey.map { String(format: "%02hhx", $0) }.joined() + } +} diff --git a/SessionUtilitiesKit/General/Array+Description.swift b/SessionUtilitiesKit/General/Array+Description.swift deleted file mode 100644 index 6ac99240a..000000000 --- a/SessionUtilitiesKit/General/Array+Description.swift +++ /dev/null @@ -1,7 +0,0 @@ - -public extension Array where Element : CustomStringConvertible { - - var prettifiedDescription: String { - return "[ " + map { $0.description }.joined(separator: ", ") + " ]" - } -} diff --git a/SessionUtilitiesKit/General/Array+Utilities.swift b/SessionUtilitiesKit/General/Array+Utilities.swift new file mode 100644 index 000000000..3a22fc210 --- /dev/null +++ b/SessionUtilitiesKit/General/Array+Utilities.swift @@ -0,0 +1,23 @@ + +public extension Array where Element : CustomStringConvertible { + + var prettifiedDescription: String { + return "[ " + map { $0.description }.joined(separator: ", ") + " ]" + } +} + +public extension Array { + func appending(_ other: Element) -> [Element] { + var updatedArray: [Element] = self + updatedArray.append(other) + + return updatedArray + } + + func appending(_ other: [Element]) -> [Element] { + var updatedArray: [Element] = self + updatedArray.append(contentsOf: other) + + return updatedArray + } +} diff --git a/SessionUtilitiesKit/General/Data+Trimming.swift b/SessionUtilitiesKit/General/Data+Trimming.swift deleted file mode 100644 index e16ebb094..000000000 --- a/SessionUtilitiesKit/General/Data+Trimming.swift +++ /dev/null @@ -1,18 +0,0 @@ - -public extension Data { - - func removingIdPrefixIfNeeded() -> Data { - var result = self - if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() } - return result - } -} - -@objc public extension NSData { - - @objc func removingIdPrefixIfNeeded() -> NSData { - var result = self as Data - if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() } - return result as NSData - } -} diff --git a/SessionUtilitiesKit/General/Data+Utilities.swift b/SessionUtilitiesKit/General/Data+Utilities.swift new file mode 100644 index 000000000..e0fddf1a3 --- /dev/null +++ b/SessionUtilitiesKit/General/Data+Utilities.swift @@ -0,0 +1,48 @@ +import Foundation + +public extension Data { + + func removingIdPrefixIfNeeded() -> Data { + var result = self + if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() } + return result + } + + func appending(_ other: Data) -> Data { + var mutableData: Data = Data() + mutableData.append(self) + mutableData.append(other) + + return mutableData + } + + func appending(_ other: [UInt8]) -> Data { + var mutableData: Data = Data() + mutableData.append(self) + mutableData.append(contentsOf: other) + + return mutableData + } +} + +@objc public extension NSData { + + @objc func removingIdPrefixIfNeeded() -> NSData { + var result = self as Data + if result.count == 33 && IdPrefix(with: result.toHexString()) != nil { result.removeFirst() } + return result as NSData + } +} + +// MARK: - Decoding + +public extension Data { + func decoded(as type: T.Type, customError: Error? = nil) throws -> T { + do { + return try JSONDecoder().decode(type, from: self) + } + catch let error { + throw (customError ?? error) + } + } +} diff --git a/SessionUtilitiesKit/General/Dictionary+Description.swift b/SessionUtilitiesKit/General/Dictionary+Description.swift deleted file mode 100644 index f402736ac..000000000 --- a/SessionUtilitiesKit/General/Dictionary+Description.swift +++ /dev/null @@ -1,13 +0,0 @@ - -public extension Dictionary { - - var prettifiedDescription: String { - return "[ " + map { key, value in - let keyDescription = String(describing: key) - let valueDescription = String(describing: value) - let maxLength = 20 - let truncatedValueDescription = valueDescription.count > maxLength ? valueDescription.prefix(maxLength) + "..." : valueDescription - return keyDescription + " : " + truncatedValueDescription - }.joined(separator: ", ") + " ]" - } -} diff --git a/SessionUtilitiesKit/General/Dictionary+Utilities.swift b/SessionUtilitiesKit/General/Dictionary+Utilities.swift new file mode 100644 index 000000000..d01b3176d --- /dev/null +++ b/SessionUtilitiesKit/General/Dictionary+Utilities.swift @@ -0,0 +1,42 @@ + +public extension Dictionary { + + var prettifiedDescription: String { + return "[ " + map { key, value in + let keyDescription = String(describing: key) + let valueDescription = String(describing: value) + let maxLength = 20 + let truncatedValueDescription = valueDescription.count > maxLength ? valueDescription.prefix(maxLength) + "..." : valueDescription + return keyDescription + " : " + truncatedValueDescription + }.joined(separator: ", ") + " ]" + } +} + +// MARK: - Functional Convenience + +public extension Dictionary { + func setting(_ key: Key, _ value: Value?) -> [Key: Value] { + var updatedDictionary: [Key: Value] = self + updatedDictionary[key] = value + + return updatedDictionary + } + + func updated(with other: [Key: Value]) -> [Key: Value] { + var updatedDictionary: [Key: Value] = self + + other.forEach { key, value in + updatedDictionary[key] = value + } + + return updatedDictionary + } + + func removingValue(forKey key: Key) -> [Key: Value] { + var updatedDictionary: [Key: Value] = self + updatedDictionary.removeValue(forKey: key) + + return updatedDictionary + } +} + diff --git a/SessionUtilitiesKit/General/IdPrefix.swift b/SessionUtilitiesKit/General/IdPrefix.swift index 640fe85c5..a49a0f9ba 100644 --- a/SessionUtilitiesKit/General/IdPrefix.swift +++ b/SessionUtilitiesKit/General/IdPrefix.swift @@ -1,8 +1,20 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import Curve25519Kit + +/// The `BlindedECKeyPair` is essentially the same as the `ECKeyPair` except it allows us to more easily distinguish between the two, +/// additionally when generating the `hexEncodedPublicKey` value it will apply the correct prefix +public class BlindedECKeyPair: ECKeyPair {} public enum IdPrefix: String, CaseIterable { case standard = "05" // Used for identified users, open groups, etc. case blinded = "15" // Used for participants in open groups + + public init?(with sessionId: String) { + guard ECKeyPair.isValidHexEncodedPublicKey(candidate: sessionId) else { return nil } + guard let targetPrefix: IdPrefix = IdPrefix(rawValue: String(sessionId.prefix(2))) else { return nil } + + self = targetPrefix + } } diff --git a/SessionUtilitiesKit/General/String+Encoding.swift b/SessionUtilitiesKit/General/String+Encoding.swift new file mode 100644 index 000000000..270f43ac2 --- /dev/null +++ b/SessionUtilitiesKit/General/String+Encoding.swift @@ -0,0 +1,18 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension String { + public func dataFromHex() -> Data? { + guard (self.count % 2) == 0 else { return nil } + + let chars = self.map { $0 } + let bytes: [UInt8] = stride(from: 0, to: chars.count, by: 2) + .map { index -> String in String(chars[index]) + String(chars[index + 1]) } + .compactMap { (str: String) -> UInt8? in UInt8(str, radix: 16) } + + guard (self.count / bytes.count) == 2 else { return nil } + + return Data(bytes) + } +} diff --git a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h index c4aca0f08..13c7076ba 100644 --- a/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h +++ b/SessionUtilitiesKit/Meta/SessionUtilitiesKit.h @@ -16,7 +16,6 @@ FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[]; #import #import #import -#import #import #import #import diff --git a/SessionUtilitiesKit/Networking/TSRequest.h b/SessionUtilitiesKit/Networking/TSRequest.h deleted file mode 100644 index 5c4f75d01..000000000 --- a/SessionUtilitiesKit/Networking/TSRequest.h +++ /dev/null @@ -1,29 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -#define textSecureHTTPTimeOut 10 - -@interface TSRequest : NSMutableURLRequest - -@property (nonatomic, readonly) NSDictionary *parameters; - -- (instancetype)init NS_UNAVAILABLE; - -- (instancetype)initWithURL:(NSURL *)URL; - -- (instancetype)initWithURL:(NSURL *)URL - cachePolicy:(NSURLRequestCachePolicy)cachePolicy - timeoutInterval:(NSTimeInterval)timeoutInterval NS_UNAVAILABLE; - -- (instancetype)initWithURL:(NSURL *)URL - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters; - -+ (instancetype)requestWithUrl:(NSURL *)url - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters; - -@end - -NS_ASSUME_NONNULL_END diff --git a/SessionUtilitiesKit/Networking/TSRequest.m b/SessionUtilitiesKit/Networking/TSRequest.m deleted file mode 100644 index 4d0951939..000000000 --- a/SessionUtilitiesKit/Networking/TSRequest.m +++ /dev/null @@ -1,64 +0,0 @@ -#import "TSRequest.h" - -NS_ASSUME_NONNULL_BEGIN - -@implementation TSRequest - -- (id)initWithURL:(NSURL *)URL { - self = [super initWithURL:URL - cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData - timeoutInterval:textSecureHTTPTimeOut]; - - if (!self) { - return nil; - } - - _parameters = @{}; - - return self; -} - -- (instancetype)init -{ - return nil; -} - -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wobjc-designated-initializers" - -- (instancetype)initWithURL:(NSURL *)URL - cachePolicy:(NSURLRequestCachePolicy)cachePolicy - timeoutInterval:(NSTimeInterval)timeoutInterval -{ - return nil; -} - -- (instancetype)initWithURL:(NSURL *)URL - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters -{ - self = [super initWithURL:URL - cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData - timeoutInterval:textSecureHTTPTimeOut]; - - if (!self) { - return nil; - } - - _parameters = parameters ?: @{}; - - [self setHTTPMethod:method]; - - return self; -} - -+ (instancetype)requestWithUrl:(NSURL *)url - method:(NSString *)method - parameters:(nullable NSDictionary *)parameters -{ - return [[TSRequest alloc] initWithURL:url method:method parameters:parameters]; -} - -@end - -NS_ASSUME_NONNULL_END diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift index 3297ce14e..57ad21e43 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift @@ -45,7 +45,20 @@ extension MessageSender { let storage = SNMessagingKitConfiguration.shared.storage if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) { let (promise, seal) = Promise.pending() - AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) }) + AttachmentUploadJob.upload( + stream, + using: { data in + OpenGroupAPIV2.upload( + data, + to: v2OpenGroup.room, + on: v2OpenGroup.server + ) + }, + encrypt: false, + onSuccess: { seal.fulfill(()) }, + onFailure: { seal.reject($0) } + ) + return promise } else { let (promise, seal) = Promise.pending() @@ -78,7 +91,19 @@ extension MessageSender { let storage = SNMessagingKitConfiguration.shared.storage if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) { let (promise, seal) = Promise.pending() - AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) }) + AttachmentUploadJob.upload( + stream, + using: { data in + OpenGroupAPIV2.upload( + data, + to: v2OpenGroup.room, + on: v2OpenGroup.server + ) + }, + encrypt: false, + onSuccess: { seal.fulfill(()) }, + onFailure: { seal.reject($0) } + ) return promise } else { let (promise, seal) = Promise.pending()