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()