Migrated the SessionSnodeKit from YapDatabase to GRDB

Changed the min OS version to iOS 13.0 (support for 'Identifiable')
Removed the alternate approaches to fetching the userKeyPair and userPublicKeyHexString (no consistently routed through the caching method)
Migrated the 'OWSIdentityManager' logic to use the new 'Identity' type
Added the 'Setting' table and got the pattern working fairly nicely (unfortunately there isn't a good way to avoid key collision without proper enums)
Updated the SessionSnodeKit to migration it's data from YDB to GRDB
Updated the SessionSnodeKit to use GRDB throughout it's logic
This commit is contained in:
Morgan Pretty 2022-04-01 17:22:45 +11:00
parent e65682ae9b
commit a1b4554cdb
87 changed files with 1822 additions and 1467 deletions

View File

@ -188,7 +188,6 @@
B8566C7D256F62030045A0B9 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2D3255B6DAF007E1867 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; };
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.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 */; };
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 */; };
@ -287,7 +286,6 @@
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; };
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 */; };
@ -348,9 +346,6 @@
C32C5BE6256DC891003C73A2 /* OWSReadReceiptManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB1D255A580900E217F9 /* OWSReadReceiptManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDD255A581900E217F9 /* OWSDisappearingMessagesJob.m */; };
C32C5BF8256DC8F6003C73A2 /* OWSDisappearingMessagesJob.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDA80255A57FC00E217F9 /* OWSDisappearingMessagesJob.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5C01256DC9A0003C73A2 /* OWSIdentityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */; };
C32C5C0A256DC9B4003C73A2 /* OWSIdentityManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5C1B256DC9E0003C73A2 /* General.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBC1255A581700E217F9 /* General.swift */; };
C32C5C24256DCB30003C73A2 /* NotificationsProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB7A255A581000E217F9 /* NotificationsProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; };
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -359,8 +354,6 @@
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */; };
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB88255A581200E217F9 /* TSAccountManager.m */; };
C32C5CAD256DD1DF003C73A2 /* TSAccountManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB94255A581300E217F9 /* TSAccountManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5CBE256DD282003C73A2 /* Storage+OnionRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D8F1BC25661C6F0092EF10 /* Storage+OnionRequests.swift */; };
C32C5CBF256DD282003C73A2 /* Storage+SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A607255C98A6007BE2A3 /* Storage+SnodeAPI.swift */; };
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */; };
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */; };
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA7E255A57FB00E217F9 /* Mention.swift */; };
@ -670,7 +663,6 @@
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */; };
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B7255385EC00C340D1 /* Snode.swift */; };
C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B8255385EC00C340D1 /* Storage.swift */; };
C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; };
C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */; };
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */; };
@ -754,6 +746,9 @@
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
FD09796927F6BEA700936362 /* SwarmSnode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796827F6BEA700936362 /* SwarmSnode.swift */; };
FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; };
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */; };
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; };
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
@ -765,7 +760,6 @@
FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; };
FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */; };
FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */; };
FD17D7B627F51E7300122BE0 /* SettingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B527F51E7300122BE0 /* SettingType.swift */; };
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; };
FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; };
FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */; };
@ -774,6 +768,15 @@
FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */; };
FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */; };
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; };
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */; };
FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7CC27F546FF00122BE0 /* Setting.swift */; };
FD17D7D227F5797A00122BE0 /* SSKEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D127F5797A00122BE0 /* SSKEndpoint.swift */; };
FD17D7D427F6584600122BE0 /* SSKError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D327F6584600122BE0 /* SSKError.swift */; };
FD17D7D827F658E200122BE0 /* SSKDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7D727F658E200122BE0 /* SSKDestination.swift */; };
FD17D7E127F67BD400122BE0 /* SnodeReceivedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */; };
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; };
FD17D7E727F6A16700122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _002_YDBToGRDBMigration.swift */; };
FD17D7EA27F6A1C600122BE0 /* SUKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacyModels.swift */; };
FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */; };
FD28A4F427EA79F800FF65E7 /* BlockListUIUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */; };
FD3C907327E8387300CD579F /* OpenGroupServerIdLookupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */; };
@ -1274,7 +1277,6 @@
B8D8F17625661AFA0092EF10 /* Storage+Jobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Jobs.swift"; sourceTree = "<group>"; };
B8D8F18825661BA50092EF10 /* Storage+OpenGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OpenGroups.swift"; sourceTree = "<group>"; };
B8D8F19225661BF80092EF10 /* Storage+Messaging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Messaging.swift"; sourceTree = "<group>"; };
B8D8F1BC25661C6F0092EF10 /* Storage+OnionRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+OnionRequests.swift"; sourceTree = "<group>"; };
B8D8F1EF256621180092EF10 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "MessageSender+Convenience.swift"; path = "../../SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift"; sourceTree = "<group>"; };
B8EB20E6263F7E4B00773E52 /* sk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sk; path = sk.lproj/Localizable.strings; sourceTree = "<group>"; };
B8EB20ED2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleMessage+OpenGroupInvitation.swift"; sourceTree = "<group>"; };
@ -1307,7 +1309,6 @@
C31D1DDC25217014005D4DA8 /* UserCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCell.swift; sourceTree = "<group>"; };
C31D1DE22521718E005D4DA8 /* UserSelectionVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSelectionVC.swift; sourceTree = "<group>"; };
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactUtilities.swift; sourceTree = "<group>"; };
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairUtilities.swift; sourceTree = "<group>"; };
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = "<group>"; };
C328250E25CA06020062D0A7 /* VoiceMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VoiceMessageView.swift; sourceTree = "<group>"; };
C328251E25CA3A900062D0A7 /* QuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteView.swift; sourceTree = "<group>"; };
@ -1461,7 +1462,6 @@
C33FDBA1255A581400E217F9 /* OWSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOperation.h; sourceTree = "<group>"; };
C33FDBA4255A581400E217F9 /* OWSDisappearingMessagesConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesConfiguration.m; sourceTree = "<group>"; };
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSLinkPreview.swift; sourceTree = "<group>"; };
C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSIdentityManager.m; sourceTree = "<group>"; };
C33FDBAB255A581500E217F9 /* OWSFileSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFileSystem.h; sourceTree = "<group>"; };
C33FDBAE255A581500E217F9 /* SignalAccount.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalAccount.h; sourceTree = "<group>"; };
C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLSessionDataTask+StatusCode.m"; sourceTree = "<group>"; };
@ -1472,7 +1472,6 @@
C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+keyFromIntLong.h"; sourceTree = "<group>"; };
C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+Loki.h"; sourceTree = "<group>"; };
C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = "<group>"; };
C33FDBC1255A581700E217F9 /* General.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = "<group>"; };
C33FDBCA255A581700E217F9 /* LKGroupUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LKGroupUtilities.h; sourceTree = "<group>"; };
C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = "<group>"; };
@ -1484,7 +1483,6 @@
C33FDBE9255A581A00E217F9 /* TSInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInteraction.m; sourceTree = "<group>"; };
C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRecipientIdentity.m; sourceTree = "<group>"; };
C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSStorageKeys.h; sourceTree = "<group>"; };
C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIdentityManager.h; sourceTree = "<group>"; };
C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLSessionDataTask+StatusCode.h"; sourceTree = "<group>"; };
C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+OWS.h"; sourceTree = "<group>"; };
C33FDBF9255A581C00E217F9 /* OWSError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSError.h; sourceTree = "<group>"; };
@ -1706,7 +1704,6 @@
C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeMessage.swift; sourceTree = "<group>"; };
C3C2A5B7255385EC00C340D1 /* Snode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snode.swift; sourceTree = "<group>"; };
C3C2A5B8255385EC00C340D1 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = "<group>"; };
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = "<group>"; };
@ -1764,7 +1761,6 @@
C3F0A5B2255C915C007BE2A3 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
C3F0A5EB255C970D007BE2A3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
C3F0A5FD255C988A007BE2A3 /* Storage+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Shared.swift"; sourceTree = "<group>"; };
C3F0A607255C98A6007BE2A3 /* Storage+SnodeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+SnodeAPI.swift"; sourceTree = "<group>"; };
C5060C3B36A848B71CCE4685 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
C88965DE4F4EC4FC919BEC4E /* Pods-SessionUIKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUIKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUIKit/Pods-SessionUIKit.debug.xcconfig"; sourceTree = "<group>"; };
C98441E849C3CA7FE8220D33 /* Pods-SessionNotificationServiceExtension.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionNotificationServiceExtension.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionNotificationServiceExtension/Pods-SessionNotificationServiceExtension.app store release.xcconfig"; sourceTree = "<group>"; };
@ -1795,6 +1791,8 @@
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD09796827F6BEA700936362 /* SwarmSnode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmSnode.swift; sourceTree = "<group>"; };
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = "<group>"; };
@ -1805,7 +1803,6 @@
FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessageInfo.swift; sourceTree = "<group>"; };
FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = "<group>"; };
FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKSetting.swift; sourceTree = "<group>"; };
FD17D7B527F51E7300122BE0 /* SettingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingType.swift; sourceTree = "<group>"; };
FD17D7B727F51ECA00122BE0 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = "<group>"; };
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetMigrations.swift; sourceTree = "<group>"; };
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GRDB+Notifications.swift"; sourceTree = "<group>"; };
@ -1814,9 +1811,18 @@
FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Utilities.swift"; sourceTree = "<group>"; };
FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColumnDefinition+Utilities.swift"; sourceTree = "<group>"; };
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = "<group>"; };
FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D7CC27F546FF00122BE0 /* Setting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Setting.swift; sourceTree = "<group>"; };
FD17D7D127F5797A00122BE0 /* SSKEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKEndpoint.swift; sourceTree = "<group>"; };
FD17D7D327F6584600122BE0 /* SSKError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKError.swift; sourceTree = "<group>"; };
FD17D7D727F658E200122BE0 /* SSKDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKDestination.swift; sourceTree = "<group>"; };
FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessage.swift; sourceTree = "<group>"; };
FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = "<group>"; };
FD17D7E627F6A16700122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D7E927F6A1C600122BE0 /* SUKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacyModels.swift; sourceTree = "<group>"; };
FD28A4F127E990E800FF65E7 /* BlockingManagerRemovalMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockingManagerRemovalMigration.swift; sourceTree = "<group>"; };
FD28A4F327EA79F800FF65E7 /* BlockListUIUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockListUIUtils.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = "<group>"; };
FD3C907227E8387300CD579F /* OpenGroupServerIdLookupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookupMigration.swift; sourceTree = "<group>"; };
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
@ -2062,7 +2068,6 @@
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
C31D1DE8252172D4005D4DA8 /* ContactUtilities.swift */,
C35E8AAD2485E51D00ACB629 /* IP2Country.swift */,
C31FFE56254A5FFE00F19441 /* KeyPairUtilities.swift */,
B84664F4235022F30083A1CD /* MentionUtilities.swift */,
B886B4A82398BA1500211ABE /* QRCode.swift */,
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */,
@ -2271,6 +2276,9 @@
B8A582AB258C64E800AFD84C /* Database */ = {
isa = PBXGroup;
children = (
FD17D7E827F6A1B800122BE0 /* LegacyDatabase */,
FD17D7C827F546CE00122BE0 /* Migrations */,
FD17D7CB27F546F500122BE0 /* Models */,
FD17D7B427F51E6700122BE0 /* Types */,
FD17D7BB27F51F5C00122BE0 /* Utilities */,
FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */,
@ -3183,7 +3191,6 @@
C37F5402255BA9ED002AEA92 /* Environment.m */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C33FDB7F255A581100E217F9 /* FullTextSearchFinder.swift */,
C33FDBC1255A581700E217F9 /* General.swift */,
B82A0C3726B9098200C1BCE3 /* MessageInvalidator.swift */,
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */,
@ -3195,8 +3202,6 @@
C33FDC1B255A581F00E217F9 /* OWSBackgroundTask.m */,
C33FDC05255A581D00E217F9 /* OWSDisappearingMessagesFinder.h */,
C33FDA86255A57FC00E217F9 /* OWSDisappearingMessagesFinder.m */,
C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */,
C33FDBA9255A581500E217F9 /* OWSIdentityManager.m */,
C33FDAC0255A580100E217F9 /* OWSIncomingMessageFinder.h */,
C33FDB1E255A580900E217F9 /* OWSIncomingMessageFinder.m */,
C33FDB67255A580F00E217F9 /* OWSMediaGalleryFinder.h */,
@ -3213,7 +3218,6 @@
C33FDB91255A581200E217F9 /* ProtoUtils.h */,
C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */,
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
C33FDB31255A580A00E217F9 /* SSKEnvironment.h */,
C33FDAF4255A580600E217F9 /* SSKEnvironment.m */,
C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */,
@ -3233,15 +3237,14 @@
children = (
C3C2A5B0255385C700C340D1 /* Meta */,
FD17D79D27F40CAA00122BE0 /* Database */,
FD17D7DF27F67BC400122BE0 /* Models */,
FD17D7D027F5795300122BE0 /* Types */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */,
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */,
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */,
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */,
C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */,
C3C2A5B8255385EC00C340D1 /* Storage.swift */,
B8D8F1BC25661C6F0092EF10 /* Storage+OnionRequests.swift */,
C3F0A607255C98A6007BE2A3 /* Storage+SnodeAPI.swift */,
C3C2A5CD255385F300C340D1 /* Utilities */,
);
path = SessionSnodeKit;
@ -3279,6 +3282,7 @@
B8A582B9258C696200AFD84C /* Messaging */,
B8A582AE258C65D000AFD84C /* Networking */,
B8A582AD258C655E00AFD84C /* PromiseKit */,
FD09796527F6B0A800936362 /* Utilities */,
C3D9E43025676D3D0040E4F3 /* Configuration.swift */,
);
path = SessionUtilitiesKit;
@ -3591,6 +3595,15 @@
path = Session;
sourceTree = "<group>";
};
FD09796527F6B0A800936362 /* Utilities */ = {
isa = PBXGroup;
children = (
FD09796A27F6C67500936362 /* Failable.swift */,
C3E7134E251C867C009649BB /* Sodium+Conversion.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FD17D79427F3E03300122BE0 /* Migrations */ = {
isa = PBXGroup;
children = (
@ -3659,7 +3672,6 @@
children = (
FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */,
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
FD17D7B527F51E7300122BE0 /* SettingType.swift */,
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
);
@ -3677,6 +3689,51 @@
path = Utilities;
sourceTree = "<group>";
};
FD17D7C827F546CE00122BE0 /* Migrations */ = {
isa = PBXGroup;
children = (
FD17D7C927F546D900122BE0 /* _001_InitialSetupMigration.swift */,
FD17D7E627F6A16700122BE0 /* _002_YDBToGRDBMigration.swift */,
);
path = Migrations;
sourceTree = "<group>";
};
FD17D7CB27F546F500122BE0 /* Models */ = {
isa = PBXGroup;
children = (
FD17D7E427F6A09900122BE0 /* Identity.swift */,
FD17D7CC27F546FF00122BE0 /* Setting.swift */,
);
path = Models;
sourceTree = "<group>";
};
FD17D7D027F5795300122BE0 /* Types */ = {
isa = PBXGroup;
children = (
FD17D7D127F5797A00122BE0 /* SSKEndpoint.swift */,
FD17D7D327F6584600122BE0 /* SSKError.swift */,
FD17D7D727F658E200122BE0 /* SSKDestination.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD17D7DF27F67BC400122BE0 /* Models */ = {
isa = PBXGroup;
children = (
FD17D7E027F67BD400122BE0 /* SnodeReceivedMessage.swift */,
FD09796827F6BEA700936362 /* SwarmSnode.swift */,
);
path = Models;
sourceTree = "<group>";
};
FD17D7E827F6A1B800122BE0 /* LegacyDatabase */ = {
isa = PBXGroup;
children = (
FD17D7E927F6A1C600122BE0 /* SUKLegacyModels.swift */,
);
path = LegacyDatabase;
sourceTree = "<group>";
};
FD659ABE27A7648200F12C02 /* Message Requests */ = {
isa = PBXGroup;
children = (
@ -3843,7 +3900,6 @@
C3A3A15F256E1BB4004D228D /* ProtoUtils.h in Headers */,
C3A3A145256E1B49004D228D /* OWSMediaGalleryFinder.h in Headers */,
B8856E33256F18D5001CE70E /* OWSStorage+Subclass.h in Headers */,
C32C5C0A256DC9B4003C73A2 /* OWSIdentityManager.h in Headers */,
B8856E9D256F1C3D001CE70E /* OWSSounds.h in Headers */,
C32C5E64256DDFD6003C73A2 /* OWSStorage.h in Headers */,
);
@ -4664,27 +4720,29 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FD09796927F6BEA700936362 /* SwarmSnode.swift in Sources */,
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */,
FD17D7E127F67BD400122BE0 /* SnodeReceivedMessage.swift in Sources */,
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */,
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */,
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */,
C32C5CBF256DD282003C73A2 /* Storage+SnodeAPI.swift in Sources */,
C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */,
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
FD17D7A427F40F8100122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
C32C5CBE256DD282003C73A2 /* Storage+OnionRequests.swift in Sources */,
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */,
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */,
FD17D7D227F5797A00122BE0 /* SSKEndpoint.swift in Sources */,
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */,
C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */,
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */,
FD17D7A727F41AF000122BE0 /* SSKLegacyModels.swift in Sources */,
C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */,
FD17D7D827F658E200122BE0 /* SSKDestination.swift in Sources */,
FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */,
FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */,
C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */,
C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */,
FD17D7D427F6584600122BE0 /* SSKError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4694,7 +4752,6 @@
files = (
C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */,
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */,
FD17D7B627F51E7300122BE0 /* SettingType.swift in Sources */,
C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */,
C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */,
C3D9E41525676C320040E4F3 /* Storage.swift in Sources */,
@ -4703,11 +4760,13 @@
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
FD17D7EA27F6A1C600122BE0 /* SUKLegacyModels.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */,
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
B8856E09256F1676001CE70E /* UIDevice+featureSupport.swift in Sources */,
B8856DEF256F161F001CE70E /* NSString+SSK.m in Sources */,
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */,
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */,
C3D9E4C02567767F0040E4F3 /* DataSource.m in Sources */,
C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */,
@ -4717,6 +4776,7 @@
C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */,
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */,
FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */,
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
@ -4727,6 +4787,7 @@
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */,
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
FD09796B27F6C67500936362 /* Failable.swift in Sources */,
FD705A92278D051200F16121 /* ReusableView.swift in Sources */,
FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */,
FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */,
@ -4753,8 +4814,10 @@
C32C5E0C256DDAFA003C73A2 /* NSRegularExpression+SSK.swift in Sources */,
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
B8856D23256F116B001CE70E /* Weak.swift in Sources */,
FD17D7CD27F546FF00122BE0 /* Setting.swift in Sources */,
C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */,
FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */,
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */,
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
C300A60D2554B31900555489 /* Logging.swift in Sources */,
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
@ -4762,6 +4825,7 @@
FD705A8E278CE29800F16121 /* String+Localization.swift in Sources */,
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
FD17D7E727F6A16700122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -4848,9 +4912,7 @@
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
C32C5AAB256DBE8F003C73A2 /* TSIncomingMessage+Conversion.swift in Sources */,
B866CE112581C1A900535CC4 /* Sodium+Conversion.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 */,
@ -4905,7 +4967,6 @@
C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */,
C32C5B62256DC333003C73A2 /* OWSDisappearingConfigurationUpdateInfoMessage.m in Sources */,
C352A2F525574B4700338F3E /* Job.swift in Sources */,
C32C5C01256DC9A0003C73A2 /* OWSIdentityManager.m in Sources */,
C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */,
C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */,
B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */,
@ -4975,7 +5036,6 @@
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
B8F5F72325F1B4CA003BF8D4 /* DownloadAttachmentModal.swift in Sources */,
C3DFFAC623E96F0D0058DAF8 /* Sheet.swift in Sources */,
C31FFE57254A5FFE00F19441 /* KeyPairUtilities.swift in Sources */,
B8D84EA325DF745A005A043E /* LinkPreviewState.swift in Sources */,
45C0DC1E1E69011F00E04C47 /* UIStoryboard+OWS.swift in Sources */,
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */,
@ -5248,7 +5308,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5326,7 +5386,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionShareExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5385,7 +5445,7 @@
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5464,7 +5524,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionNotificationServiceExtension/Meta/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5521,7 +5581,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionUIKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5600,7 +5660,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionUIKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5668,7 +5728,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SignalUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5755,7 +5815,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SignalUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5815,7 +5875,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5894,7 +5954,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionSnodeKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -5954,7 +6014,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -6042,7 +6102,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -6111,7 +6171,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
INFOPLIST_FILE = SessionMessagingKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -6190,7 +6250,7 @@
GCC_WARN_UNUSED_VARIABLE = YES;
INFOPLIST_FILE = SessionMessagingKit/Meta/Info.plist;
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -6407,7 +6467,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "Session/Meta/Session-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@ -6478,7 +6538,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "Session/Meta/Session-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",

View File

@ -1,6 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
import SessionMessagingKit
import UIKit
import SessionUtilitiesKit
// TODO:
// Slight paging glitch when scrolling up and loading more content
@ -69,13 +72,12 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
}
lazy var mnemonic: String = {
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
if hexEncodedSeed == nil {
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
}()
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: nil, delegate: self)

View File

@ -1005,7 +1005,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey];
if (![SNOpenGroupAPIV2 isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
}
@ -1058,7 +1058,7 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
// If it's an incoming message the user must have moderator status
if (self.interaction.interactionType == OWSInteractionType_IncomingMessage) {
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
NSString *userPublicKey = [SNGeneralUtilities getUserPublicKey];
if (openGroupV2 != nil) {
if (![SNOpenGroupAPIV2 isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
}

View File

@ -114,10 +114,6 @@ CGFloat kIconViewLength = 24;
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(identityStateDidChange:)
name:kNSNotificationName_IdentityStateDidChange
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(otherUsersProfileDidChange:)
name:kNSNotificationName_OtherUsersProfileDidChange
@ -1069,13 +1065,6 @@ CGFloat kIconViewLength = 24;
#pragma mark - Notifications
- (void)identityStateDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();
[self updateTableContents];
}
- (void)otherUsersProfileDidChange:(NSNotification *)notification
{
OWSAssertIsOnMainThread();

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionMessagingKit
import SessionUtilitiesKit
// See https://github.com/yapstudios/YapDatabase/wiki/LongLivedReadTransactions and
// https://github.com/yapstudios/YapDatabase/wiki/YapDatabaseModifiedNotification for
@ -162,7 +167,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
self.threads.update(with: transaction) // Perform the initial update
}
// Start polling if needed (i.e. if the user just created or restored their Session ID)
if OWSIdentityManager.shared().identityKeyPair() != nil {
if Identity.fetchUserKeyPair() != nil {
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.startPollerIfNeeded()
appDelegate.startClosedGroupPoller()

View File

@ -748,7 +748,7 @@ static NSTimeInterval launchStartedAt;
[[LKPushNotificationAPI unregisterToken:deviceToken] retainUntilComplete];
}
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[SUKIdentity clearUserKeyPair];
[SNSnodeAPI clearSnodePool];
[self stopPoller];
[self stopClosedGroupPoller];

View File

@ -1,5 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionMessagingKit
import SessionUtilitiesKit
extension AppDelegate {
@ -22,7 +26,8 @@ extension AppDelegate {
}
@objc func startClosedGroupPoller() {
guard OWSIdentityManager.shared().identityKeyPair() != nil else { return }
guard Identity.fetchUserKeyPair() != nil else { return }
ClosedGroupPoller.shared.start()
}

View File

@ -8,7 +8,6 @@
#import <SessionMessagingKit/Environment.h>
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
#import <SessionMessagingKit/OWSIdentityManager.h>
NS_ASSUME_NONNULL_BEGIN

View File

@ -63,7 +63,6 @@
#import <SignalUtilitiesKit/OWSDispatch.h>
#import <SignalUtilitiesKit/OWSError.h>
#import <SessionUtilitiesKit/OWSFileSystem.h>
#import <SessionMessagingKit/OWSIdentityManager.h>
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
#import <SessionMessagingKit/OWSRecipientIdentity.h>
#import <SignalUtilitiesKit/SignalAccount.h>

View File

@ -121,10 +121,6 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
// MARK: - Dependencies
var identityManager: OWSIdentityManager {
return OWSIdentityManager.shared()
}
var preferences: OWSPreferences {
return Environment.shared.preferences
}

View File

@ -1,5 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class LandingVC : BaseVC {
import UIKit
import SessionUIKit
final class LandingVC: BaseVC {
// MARK: Components
private lazy var fakeChatView: FakeChatView = {

View File

@ -1,4 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import PromiseKit
import SessionUtilitiesKit
final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControllerDelegate, OWSQRScannerDelegate {
private let pageVC = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
@ -130,7 +134,7 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
presentAlert(alert)
return
}
let (ed25519KeyPair, x25519KeyPair) = KeyPairUtilities.generate(from: seed)
let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed)
Onboarding.Flow.link.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
TSAccountManager.sharedInstance().didRegister()
NotificationCenter.default.addObserver(self, selector: #selector(handleInitialConfigurationMessageReceived), name: .initialConfigurationMessageReceived, object: nil)
@ -140,7 +144,8 @@ final class LinkDeviceVC : BaseVC, UIPageViewControllerDataSource, UIPageViewCon
}
@objc private func handleInitialConfigurationMessageReceived(_ notification: Notification) {
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = getUserHexEncodedPublicKey()
DispatchQueue.main.async {
self.navigationController!.dismiss(animated: true) {
let pnModeVC = PNModeVC()

View File

@ -1,4 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
enum Onboarding {
@ -7,7 +11,7 @@ enum Onboarding {
func preregister(with seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
let userDefaults = UserDefaults.standard
KeyPairUtilities.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
Identity.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519PublicKey
Storage.writeSync { transaction in
@ -16,25 +20,31 @@ enum Onboarding {
user.didApproveMe = true
Storage.shared.setContact(user, using: transaction)
}
switch self {
case .register:
userDefaults[.hasViewedSeed] = false
// Set hasSyncedInitialConfiguration to true so that when we hit the home screen a configuration sync
// is triggered (yes, the logic is a bit weird). This is needed so that if the user registers and
// immediately links a device, there'll be a configuration in their swarm.
userDefaults[.hasSyncedInitialConfiguration] = true
case .recover, .link:
userDefaults[.hasViewedSeed] = true // No need to show it again if the user is restoring or linking
userDefaults[.hasSyncedInitialConfiguration] = false
case .register:
userDefaults[.hasViewedSeed] = false
// Set hasSyncedInitialConfiguration to true so that when we hit the
// home screen a configuration sync is triggered (yes, the logic is a
// bit weird). This is needed so that if the user registers and
// immediately links a device, there'll be a configuration in their swarm.
userDefaults[.hasSyncedInitialConfiguration] = true
case .recover, .link:
// No need to show it again if the user is restoring or linking
userDefaults[.hasViewedSeed] = true
userDefaults[.hasSyncedInitialConfiguration] = false
}
switch self {
case .register, .recover:
// Set both lastDisplayNameUpdate and lastProfilePictureUpdate to the current date, so that
// we don't overwrite what the user set in the display name step with whatever we find in
// their swarm.
userDefaults[.lastDisplayNameUpdate] = Date()
userDefaults[.lastProfilePictureUpdate] = Date()
case .link: break
case .register, .recover:
// Set both lastDisplayNameUpdate and lastProfilePictureUpdate to the
// current date, so that we don't overwrite what the user set in the
// display name step with whatever we find in their swarm.
userDefaults[.lastDisplayNameUpdate] = Date()
userDefaults[.lastProfilePictureUpdate] = Date()
case .link: break
}
}
}

View File

@ -136,7 +136,7 @@ final class RegisterVC : BaseVC {
}
private func updateKeyPair() {
(ed25519KeyPair, x25519KeyPair) = KeyPairUtilities.generate(from: seed)
(ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed)
}
private func updatePublicKeyLabel() {

View File

@ -1,5 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class RestoreVC : BaseVC {
import UIKit
import SessionUtilitiesKit
final class RestoreVC: BaseVC {
private var spacer1HeightConstraint: NSLayoutConstraint!
private var spacer2HeightConstraint: NSLayoutConstraint!
private var spacer3HeightConstraint: NSLayoutConstraint!
@ -164,7 +168,7 @@ final class RestoreVC : BaseVC {
do {
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
let seed = Data(hex: hexEncodedSeed)
let (ed25519KeyPair, x25519KeyPair) = KeyPairUtilities.generate(from: seed)
let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed)
Onboarding.Flow.recover.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
mnemonicTextView.resignFirstResponder()
Timer.scheduledTimer(withTimeInterval: 0.25, repeats: false) { _ in

View File

@ -1,14 +1,16 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
final class SeedVC : BaseVC {
import UIKit
import SessionUtilitiesKit
final class SeedVC: BaseVC {
private let mnemonic: String = {
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
if hexEncodedSeed == nil {
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
}()
private lazy var redactedMnemonic: String = {

View File

@ -19,10 +19,7 @@ final class PathStatusView : UIView {
private func setUpViewHierarchy() {
layer.cornerRadius = PathStatusView.size / 2
layer.masksToBounds = false
if OnionRequestAPI.paths.isEmpty {
OnionRequestAPI.paths = Storage.shared.getOnionRequestPaths()
}
let color = (!OnionRequestAPI.paths.isEmpty) ? Colors.accent : Colors.pathsBuilding
let color = (!OnionRequestAPI.paths.isEmpty ? Colors.accent : Colors.pathsBuilding)
setColor(to: color, isAnimated: false)
}

View File

@ -106,26 +106,49 @@ final class PathVC : BaseVC {
private func update() {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
if !OnionRequestAPI.paths.isEmpty {
let pathToDisplay = OnionRequestAPI.paths.first!
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in
let isGuardSnode = (snode == pathToDisplay.first!)
return getPathRow(snode: snode, location: .middle, dotAnimationStartDelay: Double(index) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval, isGuardSnode: isGuardSnode)
}
let youRow = getPathRow(title: NSLocalizedString("vc_path_device_row_title", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let destinationRow = getPathRow(title: NSLocalizedString("vc_path_destination_row_title", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let rows = [ youRow ] + snodeRows + [ destinationRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 0
}
} else {
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else {
spinner.startAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 1
}
return
}
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in
let isGuardSnode = (snode == pathToDisplay.first)
return getPathRow(
snode: snode,
location: .middle,
dotAnimationStartDelay: Double(index) + 2,
dotAnimationRepeatInterval: dotAnimationRepeatInterval,
isGuardSnode: isGuardSnode
)
}
let youRow = getPathRow(
title: NSLocalizedString("vc_path_device_row_title", comment: ""),
subtitle: nil,
location: .top,
dotAnimationStartDelay: 1,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
let destinationRow = getPathRow(
title: NSLocalizedString("vc_path_destination_row_title", comment: ""),
subtitle: nil,
location: .bottom,
dotAnimationStartDelay: Double(pathToDisplay.count) + 2,
dotAnimationRepeatInterval: dotAnimationRepeatInterval
)
let rows = [ youRow ] + snodeRows + [ destinationRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 0
}
}
@ -156,7 +179,7 @@ final class PathVC : BaseVC {
return stackView
}
private func getPathRow(snode: Legacy.Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..."
let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "")
return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)

View File

@ -1,15 +1,18 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUtilitiesKit
@objc(LKSeedModal)
final class SeedModal : Modal {
final class SeedModal: Modal {
private let mnemonic: String = {
let identityManager = OWSIdentityManager.shared()
let databaseConnection = identityManager.value(forKey: "dbConnection") as! YapDatabaseConnection
var hexEncodedSeed: String! = databaseConnection.object(forKey: "LKLokiSeed", inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) as! String?
if hexEncodedSeed == nil {
hexEncodedSeed = identityManager.identityKeyPair()!.hexEncodedPrivateKey // Legacy account
if let hexEncodedSeed: String = Identity.fetchHexEncodedSeed() {
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
}
return Mnemonic.encode(hexEncodedString: hexEncodedSeed)
// Legacy account
return Mnemonic.encode(hexEncodedString: Identity.fetchUserKeyPair()!.hexEncodedPrivateKey)
}()
// MARK: Lifecycle

View File

@ -42,18 +42,29 @@ public final class BackgroundPoller : NSObject {
guard let snode = swarm.randomElement() else { throw SnodeAPI.Error.generic }
return attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.main) {
return SnodeAPI.getRawMessages(from: snode, associatedWith: publicKey).then(on: DispatchQueue.main) { rawResponse -> Promise<Void> in
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let promises = messages.compactMap { json -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process if one of the
// messages failed to parse.
guard let envelope = SNProtoEnvelope.from(json),
let data = try? envelope.serializedData() else { return nil }
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: true)
let messages: [SnodeReceivedMessage] = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: publicKey)
let promises = messages.compactMap { message -> Promise<Void>? in
// Use a best attempt approach here; we don't want to fail the entire process
// if one of the messages failed to parse
guard
let envelope = SNProtoEnvelope.from(message),
let data = try? envelope.serializedData()
else { return nil }
let job = MessageReceiveJob(
data: data,
serverHash: message.info.hash,
isBackgroundPoll: true
)
return job.execute()
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: lastRawMessage)
// Now that the MessageReceiveJob's have been created we can persist the received messages
if !messages.isEmpty {
GRDBStorage.shared.write { db in
messages.forEach { try? $0.info.save(db) }
}
}
return when(fulfilled: promises) // The promise returned by MessageReceiveJob never rejects
}

View File

@ -1,3 +1,6 @@
import Foundation
import GRDB
import SessionSnodeKit
final class IP2Country {
var countryNamesCache: [String:String] = [:]
@ -53,12 +56,8 @@ final class IP2Country {
}
func populateCacheIfNeeded() -> Bool {
if OnionRequestAPI.paths.isEmpty {
OnionRequestAPI.paths = Storage.shared.getOnionRequestPaths()
}
let paths = OnionRequestAPI.paths
guard !paths.isEmpty else { return false }
let pathToDisplay = paths.first!
guard let pathToDisplay: [Snode] = OnionRequestAPI.paths.first else { return false }
pathToDisplay.forEach { snode in
let _ = self.cacheCountry(for: snode.ip) // Preload if needed
}

View File

@ -1,28 +0,0 @@
import Sodium
enum KeyPairUtilities {
static func generate(from seed: Data) -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
assert(seed.count == 16)
let padding = Data(repeating: 0, count: 16)
let ed25519KeyPair = Sodium().sign.keyPair(seed: (seed + padding).bytes)!
let x25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey)!
let x25519SecretKey = Sodium().sign.toX25519(ed25519SecretKey: ed25519KeyPair.secretKey)!
let x25519KeyPair = try! ECKeyPair(publicKeyData: Data(x25519PublicKey), privateKeyData: Data(x25519SecretKey))
return (ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
}
static func store(seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
let dbConnection = OWSIdentityManager.shared().dbConnection
let collection = OWSPrimaryStorageIdentityKeyStoreCollection
dbConnection.setObject(seed.toHexString(), forKey: LKSeedKey, inCollection: collection)
dbConnection.setObject(ed25519KeyPair.secretKey.toHexString(), forKey: LKED25519SecretKey, inCollection: collection)
dbConnection.setObject(ed25519KeyPair.publicKey.toHexString(), forKey: LKED25519PublicKey, inCollection: collection)
dbConnection.setObject(x25519KeyPair, forKey: OWSPrimaryStorageIdentityKeyStoreIdentityKey, inCollection: collection)
}
static func hasV2KeyPair() -> Bool {
let dbConnection = OWSIdentityManager.shared().dbConnection
return (dbConnection.object(forKey: LKED25519SecretKey, inCollection: OWSPrimaryStorageIdentityKeyStoreCollection) != nil)
}
}

View File

@ -106,7 +106,7 @@ enum MockDataGenerator {
(0..<dmThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
let randomSessionId: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let randomSessionId: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
@ -158,7 +158,7 @@ enum MockDataGenerator {
(0..<closedGroupThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let groupName: String = (0..<groupNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
@ -171,7 +171,7 @@ enum MockDataGenerator {
(0..<numGroupMembers).forEach { _ in
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
let randomSessionId: String = KeyPairUtilities.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
let randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let contact = Contact(sessionID: randomSessionId)
contact.name = (0..<contactNameLength)
@ -229,7 +229,7 @@ enum MockDataGenerator {
(0..<openGroupThreadCount).forEach { threadIndex in
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let serverName: String = (0..<serverNameLength)

View File

@ -17,30 +17,10 @@ struct Place: Codable, FetchableRecord, PersistableRecord, ColumnExpressible {
let name: String
}
struct Setting: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
static var databaseTableName: String { "settings" }
public enum Columns: String, CodingKey, ColumnExpression {
case key
case value
}
let key: String
let value: Data
}
enum _001_InitialSetupMigration: Migration {
static let identifier: String = "initialSetup"
static func migrate(_ db: Database) throws {
try db.create(table: Setting.self) { t in
t.column(.key, .text)
.notNull()
.unique(onConflict: .abort)
.primaryKey()
t.column(.value, .blob).notNull()
}
try db.create(table: Place.self) { t in
t.column(.id, .text).notNull().primaryKey()
t.column(.name, .text).notNull()

View File

@ -1,3 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
extension Storage {

View File

@ -27,7 +27,7 @@ extension Storage {
guard let threadID = getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: groupPublicKey, openGroupID: openGroupID, using: transaction),
let thread = TSThread.fetch(uniqueId: threadID, transaction: transaction) else { return nil }
let tsMessage: TSMessage
if message.sender == getUserPublicKey() {
if message.sender == getUserHexEncodedPublicKey() {
if TSOutgoingMessage.find(withTimestamp: message.sentTimestamp!) != nil { return nil }
let tsOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
var recipients: [String] = []

View File

@ -17,24 +17,6 @@ extension Storage {
Storage.writeSync { block($0) }
}
@objc public func getUserPublicKey() -> String? {
return OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey
}
public func getUserKeyPair() -> ECKeyPair? {
return OWSIdentityManager.shared().identityKeyPair()
}
public func getUserED25519KeyPair() -> Box.KeyPair? {
let dbConnection = OWSIdentityManager.shared().dbConnection
let collection = OWSPrimaryStorageIdentityKeyStoreCollection
guard let hexEncodedPublicKey = dbConnection.object(forKey: LKED25519PublicKey, inCollection: collection) as? String,
let hexEncodedSecretKey = dbConnection.object(forKey: LKED25519SecretKey, inCollection: collection) as? String else { return nil }
let publicKey = Box.KeyPair.PublicKey(hex: hexEncodedPublicKey)
let secretKey = Box.KeyPair.SecretKey(hex: hexEncodedSecretKey)
return Box.KeyPair(publicKey: publicKey, secretKey: secretKey)
}
@objc public func getUser() -> Contact? {
return getUser(using: nil)
}

View File

@ -1,3 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import SessionUtilitiesKit
public final class ClosedGroupControlMessage : ControlMessage {

View File

@ -1,3 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import SessionUtilitiesKit
@objc(SNConfigurationMessage)

View File

@ -14,7 +14,6 @@ FOUNDATION_EXPORT const unsigned char SessionMessagingKitVersionString[];
#import <SessionMessagingKit/OWSDisappearingMessagesConfiguration.h>
#import <SessionMessagingKit/OWSDisappearingMessagesFinder.h>
#import <SessionMessagingKit/OWSDisappearingMessagesJob.h>
#import <SessionMessagingKit/OWSIdentityManager.h>
#import <SessionMessagingKit/OWSIncomingMessageFinder.h>
#import <SessionMessagingKit/OWSMediaGalleryFinder.h>
#import <SessionMessagingKit/OWSOutgoingReceiptManager.h>

View File

@ -1,10 +1,15 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Curve25519Kit
import SessionSnodeKit
import SessionUtilitiesKit
@objc(SNOpenGroupAPIV2)
public final class OpenGroupAPIV2 : NSObject {
private static var authTokenPromises: [String:Promise<String>] = [:]
private static var hasPerformedInitialPoll: [String:Bool] = [:]
private static var authTokenPromises: [String: Promise<String>] = [:]
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<String>]] = [:] // Server URL to room ID to set of moderator IDs
@ -237,7 +242,7 @@ public final class OpenGroupAPIV2 : NSObject {
public static func requestNewAuthToken(for room: String, on server: String) -> Promise<String> {
SNLog("Requesting auth token for server: \(server).")
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
guard let userKeyPair = Identity.fetchUserKeyPair() 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

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import SessionUtilitiesKit
public struct OpenGroupMessageV2 {
public let serverID: Int64?
@ -10,8 +15,11 @@ public struct OpenGroupMessageV2 {
public let base64EncodedSignature: String?
public func sign() -> OpenGroupMessageV2? {
let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()!
let data = Data(base64Encoded: base64EncodedData)!
guard
let userKeyPair = Identity.fetchUserKeyPair(),
let data: 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

View File

@ -1,6 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoSwift
import SessionUtilitiesKit
import Sodium
import Curve25519Kit
import SessionUtilitiesKit
extension MessageReceiver {

View File

@ -1,3 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import SignalCoreKit
import SessionSnodeKit
@ -556,7 +560,7 @@ extension MessageReceiver {
let groupPublicKey = explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey else { return }
let transaction = transaction as! YapDatabaseReadWriteTransaction
let userPublicKey = getUserHexEncodedPublicKey()
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else {
guard let userKeyPair = Identity.fetchUserKeyPair() else {
return SNLog("Couldn't find user X25519 key pair.")
}
let groupID = LKGroupUtilities.getEncodedClosedGroupIDAsData(groupPublicKey)

View File

@ -1,7 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
public enum MessageReceiver {
private static var lastEncryptionKeyPairRequest: [String:Date] = [:]
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
public enum Error : LocalizedError {
case duplicateMessage
@ -49,7 +52,7 @@ public enum MessageReceiver {
}
public static func parse(_ data: Data, openGroupMessageServerID: UInt64?, isRetry: Bool = false, using transaction: Any) throws -> (Message, SNProtoContent) {
let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey()
let userPublicKey = getUserHexEncodedPublicKey()
let isOpenGroupMessage = (openGroupMessageServerID != nil)
// Parse the envelope
let envelope = try SNProtoEnvelope.parseData(data)
@ -64,7 +67,7 @@ public enum MessageReceiver {
} else {
switch envelope.type {
case .sessionMessage:
guard let userX25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { throw Error.noUserX25519KeyPair }
guard let userX25519KeyPair = Identity.fetchUserKeyPair() else { throw Error.noUserX25519KeyPair }
(plaintext, sender) = try decryptWithSessionProtocol(ciphertext: ciphertext, using: userX25519KeyPair)
case .closedGroupMessage:
guard let hexEncodedGroupPublicKey = envelope.source, SNMessagingKitConfiguration.shared.storage.isClosedGroup(hexEncodedGroupPublicKey) else { throw Error.invalidGroupPublicKey }

View File

@ -1,7 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
import PromiseKit
extension MessageSender {
public static var distributingClosedGroupEncryptionKeyPairs: [String:[ECKeyPair]] = [:]
public static var distributingClosedGroupEncryptionKeyPairs: [String: [ECKeyPair]] = [:]
public static func createClosedGroup(name: String, members: Set<String>, transaction: YapDatabaseReadWriteTransaction) -> Promise<TSGroupThread> {
// Prepare

View File

@ -1,10 +1,13 @@
import SessionUtilitiesKit
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
extension MessageSender {
internal static func encryptWithSessionProtocol(_ plaintext: Data, for recipientHexEncodedX25519PublicKey: String) throws -> Data {
guard let userED25519KeyPair = SNMessagingKitConfiguration.shared.storage.getUserED25519KeyPair() else { throw Error.noUserED25519KeyPair }
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { throw Error.noUserED25519KeyPair }
let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removing05PrefixIfNeeded())
let sodium = Sodium()

View File

@ -108,7 +108,7 @@ public final class MessageSender : NSObject {
let (promise, seal) = Promise<Void>.pending()
let storage = SNMessagingKitConfiguration.shared.storage
let transaction = transaction as! YapDatabaseReadWriteTransaction
let userPublicKey = storage.getUserPublicKey()
let userPublicKey = getUserHexEncodedPublicKey()
var isMainAppAndActive = false
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
@ -267,7 +267,7 @@ 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()
message.sender = getUserHexEncodedPublicKey()
switch destination {
case .contact(_): preconditionFailure()
case .closedGroup(_): preconditionFailure()

View File

@ -100,26 +100,29 @@ public final class ClosedGroupPoller : NSObject {
private func poll(_ groupPublicKey: String) -> Promise<Void> {
guard isPolling(for: groupPublicKey) else { return Promise.value(()) }
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<(SessionSnodeKit.Legacy.Snode, [JSON], JSON?)> in
let promise = SnodeAPI.getSwarm(for: groupPublicKey).then2 { [weak self] swarm -> Promise<(Snode, [SnodeReceivedMessage])> in
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let snode = swarm.randomElement() else { return Promise(error: Error.insufficientSnodes) }
guard let self = self, self.isPolling(for: groupPublicKey) else { return Promise(error: Error.pollingCanceled) }
guard let self = self, self.isPolling(for: groupPublicKey) else {
return Promise(error: Error.pollingCanceled)
}
return SnodeAPI.getRawMessages(from: snode, associatedWith: groupPublicKey).map2 {
let (rawMessages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
let messages: [SnodeReceivedMessage] = SnodeAPI.parseRawMessagesResponse($0, from: snode, associatedWith: groupPublicKey)
return (snode, rawMessages, lastRawMessage)
return (snode, messages)
}
}
promise.done2 { [weak self] snode, rawMessages, lastRawMessage in
promise.done2 { [weak self] snode, messages in
guard let self = self, self.isPolling(for: groupPublicKey) else { return }
if !rawMessages.isEmpty {
SNLog("Received \(rawMessages.count) new message(s) in closed group with public key: \(groupPublicKey).")
if !messages.isEmpty {
SNLog("Received \(messages.count) new message(s) in closed group with public key: \(groupPublicKey).")
}
rawMessages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
messages.forEach { message in
guard let envelope = SNProtoEnvelope.from(message) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: false)
let job = MessageReceiveJob(data: data, serverHash: message.info.hash, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
@ -128,8 +131,12 @@ public final class ClosedGroupPoller : NSObject {
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: groupPublicKey, from: lastRawMessage)
// Now that the MessageReceiveJob's have been created we can persist the received messages
if !messages.isEmpty {
GRDBStorage.shared.write { db in
messages.forEach { try? $0.info.save(db) }
}
}
}
promise.catch2 { error in
SNLog("Polling failed for closed group with public key: \(groupPublicKey) due to error: \(error).")

View File

@ -5,7 +5,7 @@ import PromiseKit
public final class Poller : NSObject {
private let storage = OWSPrimaryStorage.shared()
private var isPolling = false
private var usedSnodes = Set<SessionSnodeKit.Legacy.Snode>()
private var usedSnodes = Set<Snode>()
private var pollCount = 0
// MARK: Settings
@ -66,7 +66,7 @@ public final class Poller : NSObject {
private func pollNextSnode(seal: Resolver<Void>) {
let userPublicKey = getUserHexEncodedPublicKey()
let swarm = SnodeAPI.swarmCache[userPublicKey] ?? []
let unusedSnodes = Set(swarm).subtracting(usedSnodes)
let unusedSnodes = swarm.subtracting(usedSnodes)
if !unusedSnodes.isEmpty {
// randomElement() uses the system's default random generator, which is cryptographically secure
let nextSnode = unusedSnodes.randomElement()!
@ -89,20 +89,20 @@ public final class Poller : NSObject {
}
}
private func poll(_ snode: SessionSnodeKit.Legacy.Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
guard isPolling else { return Promise { $0.fulfill(()) } }
let userPublicKey = getUserHexEncodedPublicKey()
return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: Threading.pollerQueue) { [weak self] rawResponse -> Promise<Void> in
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
let (messages, lastRawMessage) = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
let messages: [SnodeReceivedMessage] = SnodeAPI.parseRawMessagesResponse(rawResponse, from: snode, associatedWith: userPublicKey)
if !messages.isEmpty {
SNLog("Received \(messages.count) new message(s).")
}
messages.forEach { json in
guard let envelope = SNProtoEnvelope.from(json) else { return }
messages.forEach { message in
guard let envelope = SNProtoEnvelope.from(message) else { return }
do {
let data = try envelope.serializedData()
let job = MessageReceiveJob(data: data, serverHash: json["hash"] as? String, isBackgroundPoll: false)
let job = MessageReceiveJob(data: data, serverHash: message.info.hash, isBackgroundPoll: false)
SNMessagingKitConfiguration.shared.storage.write { transaction in
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
}
@ -111,17 +111,22 @@ public final class Poller : NSObject {
}
}
// Now that the MessageReceiveJob's have been created we can update the `lastMessageHash` value
SnodeAPI.updateLastMessageHashValueIfPossible(for: snode, associatedWith: userPublicKey, from: lastRawMessage)
// Now that the MessageReceiveJob's have been created we can persist the received messages
if !messages.isEmpty {
GRDBStorage.shared.write { db in
messages.forEach { try? $0.info.save(db) }
}
}
strongSelf.pollCount += 1
if strongSelf.pollCount == Poller.maxPollCount {
guard strongSelf.pollCount < Poller.maxPollCount else {
throw Error.pollLimitReached
} else {
return withDelay(Poller.pollInterval, completionQueue: Threading.pollerQueue) {
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
return strongSelf.poll(snode, seal: longTermSeal)
}
}
return withDelay(Poller.pollInterval, completionQueue: Threading.pollerQueue) {
guard let strongSelf = self, strongSelf.isPolling else { return Promise { $0.fulfill(()) } }
return strongSelf.poll(snode, seal: longTermSeal)
}
}
}

View File

@ -13,9 +13,6 @@ public protocol SessionMessagingKitStorageProtocol {
// MARK: - General
func getUserPublicKey() -> String?
func getUserKeyPair() -> ECKeyPair?
func getUserED25519KeyPair() -> Box.KeyPair?
func getUser() -> Contact?
func getUser(using transaction: YapDatabaseReadTransaction?) -> Contact?
func getAllContacts() -> Set<Contact>

View File

@ -3,7 +3,6 @@
//
#import "OWSRecipientIdentity.h"
#import "OWSIdentityManager.h"
#import "OWSPrimaryStorage.h"
#import <SignalCoreKit/Cryptography.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>

View File

@ -1,25 +0,0 @@
import Foundation
public enum General {
public enum Cache {
public static var cachedEncodedPublicKey: Atomic<String?> = Atomic(nil)
}
}
@objc(SNGeneralUtilities)
public class GeneralUtilities: NSObject {
@objc public static func getUserPublicKey() -> String {
return getUserHexEncodedPublicKey()
}
}
public func getUserHexEncodedPublicKey() -> String {
if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey }
if let keyPair = OWSIdentityManager.shared().identityKeyPair() { // Can be nil under some circumstances
General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey }
return keyPair.hexEncodedPublicKey
}
return ""
}

View File

@ -1,74 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import <Curve25519Kit/Curve25519.h>
#import <YapDatabase/YapDatabase.h>
@class OWSPrimaryStorage;
NS_ASSUME_NONNULL_BEGIN
extern NSString *const OWSPrimaryStorageIdentityKeyStoreIdentityKey;
extern NSString *const LKSeedKey;
extern NSString *const LKED25519SecretKey;
extern NSString *const LKED25519PublicKey;
extern NSString *const OWSPrimaryStorageIdentityKeyStoreCollection;
extern NSString *const OWSPrimaryStorageTrustedKeysCollection;
// This notification will be fired whenever identities are created
// or their verification state changes.
extern NSString *const kNSNotificationName_IdentityStateDidChange;
// number of bytes in a signal identity key, excluding the key-type byte.
extern const NSUInteger kIdentityKeyLength;
#ifdef DEBUG
extern const NSUInteger kStoredIdentityKeyLength;
#endif
@class OWSRecipientIdentity;
@class OWSStorage;
@class SNProtoVerified;
@class YapDatabaseReadWriteTransaction;
// This class can be safely accessed and used from any thread.
@interface OWSIdentityManager : NSObject
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
+ (instancetype)sharedManager;
- (void)generateNewIdentityKeyPair;
- (void)clearIdentityKey;
- (nullable OWSRecipientIdentity *)recipientIdentityForRecipientId:(NSString *)recipientId;
/**
* @param recipientId unique stable identifier for the recipient, e.g. e164 phone number
* @returns nil if the recipient does not exist, or is trusted for sending
* else returns the untrusted recipient.
*/
- (nullable OWSRecipientIdentity *)untrustedIdentityForSendingToRecipientId:(NSString *)recipientId;
- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId;
- (nullable ECKeyPair *)identityKeyPair;
#pragma mark - Debug
#if DEBUG
// Clears everything except the local identity key.
- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
#endif
@end
NS_ASSUME_NONNULL_END

View File

@ -1,405 +0,0 @@
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSIdentityManager.h"
#import "AppContext.h"
#import "AppReadiness.h"
#import "NSNotificationCenter+OWS.h"
#import "NotificationsProtocol.h"
#import "OWSFileSystem.h"
#import "OWSPrimaryStorage.h"
#import "OWSRecipientIdentity.h"
#import "OWSIdentityManager.h"
#import "SSKEnvironment.h"
#import "TSAccountManager.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSMessage.h"
#import "YapDatabaseConnection+OWS.h"
#import "YapDatabaseTransaction+OWS.h"
#import <Curve25519Kit/Curve25519.h>
#import <SignalCoreKit/NSDate+OWS.h>
#import <SessionMessagingKit/SessionMessagingKit-Swift.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
#import <YapDatabase/YapDatabase.h>
NS_ASSUME_NONNULL_BEGIN
// Storing our own identity key
NSString *const OWSPrimaryStorageIdentityKeyStoreIdentityKey = @"TSStorageManagerIdentityKeyStoreIdentityKey";
NSString *const LKSeedKey = @"LKLokiSeed";
NSString *const LKED25519SecretKey = @"LKED25519SecretKey";
NSString *const LKED25519PublicKey = @"LKED25519PublicKey";
NSString *const OWSPrimaryStorageIdentityKeyStoreCollection = @"TSStorageManagerIdentityKeyStoreCollection";
// Storing recipients identity keys
NSString *const OWSPrimaryStorageTrustedKeysCollection = @"TSStorageManagerTrustedKeysCollection";
NSString *const OWSIdentityManager_QueuedVerificationStateSyncMessages =
@"OWSIdentityManager_QueuedVerificationStateSyncMessages";
// Don't trust an identity for sending to unless they've been around for at least this long
const NSTimeInterval kIdentityKeyStoreNonBlockingSecondsThreshold = 5.0;
// The canonical key includes 32 bytes of identity material plus one byte specifying the key type
const NSUInteger kIdentityKeyLength = 33;
// Cryptographic operations do not use the "type" byte of the identity key, so, for legacy reasons we store just
// the identity material.
// TODO: migrate to storing the full 33 byte representation.
const NSUInteger kStoredIdentityKeyLength = 32;
NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationName_IdentityStateDidChange";
@interface OWSIdentityManager ()
@property (nonatomic, readonly) OWSPrimaryStorage *primaryStorage;
@end
#pragma mark -
@implementation OWSIdentityManager
+ (instancetype)sharedManager
{
return SSKEnvironment.shared.identityManager;
}
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
{
self = [super init];
if (!self) {
return self;
}
_primaryStorage = primaryStorage;
_dbConnection = primaryStorage.newDatabaseConnection;
self.dbConnection.objectCacheEnabled = NO;
[self observeNotifications];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark -
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)generateNewIdentityKeyPair
{
ECKeyPair *keyPair = [Curve25519 generateKeyPair];
[self.dbConnection setObject:keyPair forKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey inCollection:OWSPrimaryStorageIdentityKeyStoreCollection];
}
- (void)clearIdentityKey
{
[self.dbConnection removeObjectForKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey
inCollection:OWSPrimaryStorageIdentityKeyStoreCollection];
}
- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId
{
__block NSData *_Nullable result = nil;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
result = [self identityKeyForRecipientId:recipientId transaction:transaction];
}];
return result;
}
- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId protocolContext:(nullable id)protocolContext
{
YapDatabaseReadTransaction *transaction = protocolContext;
return [self identityKeyForRecipientId:recipientId transaction:transaction];
}
- (nullable NSData *)identityKeyForRecipientId:(NSString *)recipientId
transaction:(YapDatabaseReadTransaction *)transaction
{
return [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction].identityKey;
}
- (nullable ECKeyPair *)identityKeyPair
{
__block ECKeyPair *_Nullable identityKeyPair = nil;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
identityKeyPair = [self identityKeyPairWithTransaction:transaction];
}];
return identityKeyPair;
}
// This method should only be called from SignalProtocolKit, which doesn't know about YapDatabaseTransactions.
// Whenever possible, prefer to call the strongly typed variant: `identityKeyPairWithTransaction:`.
- (nullable ECKeyPair *)identityKeyPair:(nullable id)protocolContext
{
YapDatabaseReadTransaction *transaction = protocolContext;
return [self identityKeyPairWithTransaction:transaction];
}
- (nullable ECKeyPair *)identityKeyPairWithTransaction:(YapDatabaseReadTransaction *)transaction
{
ECKeyPair *_Nullable identityKeyPair = [transaction keyPairForKey:OWSPrimaryStorageIdentityKeyStoreIdentityKey
inCollection:OWSPrimaryStorageIdentityKeyStoreCollection];
return identityKeyPair;
}
- (int)localRegistrationId:(nullable id)protocolContext
{
YapDatabaseReadWriteTransaction *transaction = protocolContext;
return (int)[TSAccountManager getOrGenerateRegistrationId:transaction];
}
- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId
{
__block BOOL result;
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
result = [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction];
}];
return result;
}
- (BOOL)saveRemoteIdentity:(NSData *)identityKey
recipientId:(NSString *)recipientId
protocolContext:(nullable id)protocolContext
{
YapDatabaseReadWriteTransaction *transaction = protocolContext;
// Deprecated. We actually no longer use the OWSPrimaryStorageTrustedKeysCollection for trust
// decisions, but it's desirable to try to keep it up to date with our trusted identitys
// while we're switching between versions, e.g. so we don't get into a state where we have a
// session for an identity not in our key store.
[transaction setObject:identityKey forKey:recipientId inCollection:OWSPrimaryStorageTrustedKeysCollection];
OWSRecipientIdentity *existingIdentity =
[OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction];
if (existingIdentity == nil) {
[[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId
identityKey:identityKey
isFirstKnownKey:YES
createdAt:[NSDate new]
verificationState:OWSVerificationStateDefault]
saveWithTransaction:transaction];
[self fireIdentityStateChangeNotification];
return NO;
}
if (![existingIdentity.identityKey isEqual:identityKey]) {
OWSVerificationState verificationState;
switch (existingIdentity.verificationState) {
case OWSVerificationStateDefault:
verificationState = OWSVerificationStateDefault;
break;
case OWSVerificationStateVerified:
case OWSVerificationStateNoLongerVerified:
verificationState = OWSVerificationStateNoLongerVerified;
break;
}
[[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId
identityKey:identityKey
isFirstKnownKey:NO
createdAt:[NSDate new]
verificationState:verificationState] saveWithTransaction:transaction];
[self fireIdentityStateChangeNotification];
return YES;
}
return NO;
}
- (nullable OWSRecipientIdentity *)recipientIdentityForRecipientId:(NSString *)recipientId
{
__block OWSRecipientIdentity *_Nullable result;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
result = [OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction];
}];
return result;
}
- (nullable OWSRecipientIdentity *)untrustedIdentityForSendingToRecipientId:(NSString *)recipientId
{
__block OWSRecipientIdentity *_Nullable result;
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
OWSRecipientIdentity *_Nullable recipientIdentity =
[OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction];
if (recipientIdentity == nil) {
// trust on first use
return;
}
BOOL isTrusted = [self isTrustedIdentityKey:recipientIdentity.identityKey
recipientId:recipientId
direction:TSMessageDirectionOutgoing
transaction:transaction];
if (isTrusted) {
return;
} else {
result = recipientIdentity;
}
}];
return result;
}
- (void)fireIdentityStateChangeNotification
{
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:kNSNotificationName_IdentityStateDidChange
object:nil
userInfo:nil];
}
- (BOOL)isTrustedIdentityKey:(NSData *)identityKey
recipientId:(NSString *)recipientId
direction:(TSMessageDirection)direction
protocolContext:(nullable id)protocolContext
{
YapDatabaseReadWriteTransaction *transaction = protocolContext;
return [self isTrustedIdentityKey:identityKey recipientId:recipientId direction:direction transaction:transaction];
}
- (BOOL)isTrustedIdentityKey:(NSData *)identityKey
recipientId:(NSString *)recipientId
direction:(TSMessageDirection)direction
transaction:(YapDatabaseReadTransaction *)transaction
{
if ([[TSAccountManager localNumber] isEqualToString:recipientId]) {
ECKeyPair *_Nullable localIdentityKeyPair = [self identityKeyPairWithTransaction:transaction];
if ([localIdentityKeyPair.publicKey isEqualToData:identityKey]) {
return YES;
} else {
return NO;
}
}
switch (direction) {
case TSMessageDirectionIncoming: {
return YES;
}
case TSMessageDirectionOutgoing: {
OWSRecipientIdentity *existingIdentity =
[OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction];
return [self isTrustedKey:identityKey forSendingToIdentity:existingIdentity];
}
default: {
return NO;
}
}
}
- (BOOL)isTrustedKey:(NSData *)identityKey forSendingToIdentity:(nullable OWSRecipientIdentity *)recipientIdentity
{
if (recipientIdentity == nil) {
return YES;
}
if (![recipientIdentity.identityKey isEqualToData:identityKey]) {
return NO;
}
if ([recipientIdentity isFirstKnownKey]) {
return YES;
}
switch (recipientIdentity.verificationState) {
case OWSVerificationStateDefault: {
BOOL isNew = (fabs([recipientIdentity.createdAt timeIntervalSinceNow])
< kIdentityKeyStoreNonBlockingSecondsThreshold);
if (isNew) {
return NO;
} else {
return YES;
}
}
case OWSVerificationStateVerified:
return YES;
case OWSVerificationStateNoLongerVerified:
return NO;
}
}
#pragma mark - Debug
#if DEBUG
- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction
{
NSMutableArray<NSString *> *identityKeysToRemove = [NSMutableArray new];
[transaction enumerateKeysInCollection:OWSPrimaryStorageIdentityKeyStoreCollection
usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
if ([key isEqualToString:OWSPrimaryStorageIdentityKeyStoreIdentityKey]) {
// Don't delete our own key.
return;
}
[identityKeysToRemove addObject:key];
}];
for (NSString *key in identityKeysToRemove) {
[transaction removeObjectForKey:key inCollection:OWSPrimaryStorageIdentityKeyStoreCollection];
}
[transaction removeAllObjectsInCollection:OWSPrimaryStorageTrustedKeysCollection];
}
- (NSString *)identityKeySnapshotFilePath
{
// Prefix name with period "." so that backups will ignore these snapshots.
NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath];
return [dirPath stringByAppendingPathComponent:@".identity-key-snapshot"];
}
- (NSString *)trustedKeySnapshotFilePath
{
// Prefix name with period "." so that backups will ignore these snapshots.
NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath];
return [dirPath stringByAppendingPathComponent:@".trusted-key-snapshot"];
}
- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction
{
[transaction snapshotCollection:OWSPrimaryStorageIdentityKeyStoreCollection
snapshotFilePath:self.identityKeySnapshotFilePath];
[transaction snapshotCollection:OWSPrimaryStorageTrustedKeysCollection
snapshotFilePath:self.trustedKeySnapshotFilePath];
}
- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction
{
[transaction restoreSnapshotOfCollection:OWSPrimaryStorageIdentityKeyStoreCollection
snapshotFilePath:self.identityKeySnapshotFilePath];
[transaction restoreSnapshotOfCollection:OWSPrimaryStorageTrustedKeysCollection
snapshotFilePath:self.trustedKeySnapshotFilePath];
}
#endif
#pragma mark - Notifications
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
}
@end
NS_ASSUME_NONNULL_END

View File

@ -1,15 +1,15 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionSnodeKit
public extension SNProtoEnvelope {
static func from(_ json: JSON) -> SNProtoEnvelope? {
guard let base64EncodedData = json["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
SNLog("Failed to decode data for message: \(json).")
return nil
}
guard let result = try? MessageWrapper.unwrap(data: data) else {
SNLog("Failed to unwrap data for message: \(json).")
static func from(_ message: SnodeReceivedMessage) -> SNProtoEnvelope? {
guard let result = try? MessageWrapper.unwrap(data: message.data) else {
SNLog("Failed to unwrap data for message: \(String(reflecting: message)).")
return nil
}
return result
}
}

View File

@ -38,7 +38,6 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)initWithProfileManager:(id<ProfileManagerProtocol>)profileManager
primaryStorage:(OWSPrimaryStorage *)primaryStorage
identityManager:(OWSIdentityManager *)identityManager
tsAccountManager:(TSAccountManager *)tsAccountManager
disappearingMessagesJob:(OWSDisappearingMessagesJob *)disappearingMessagesJob
readReceiptManager:(OWSReadReceiptManager *)readReceiptManager

View File

@ -14,7 +14,6 @@ static SSKEnvironment *sharedSSKEnvironment;
@property (nonatomic) id<ProfileManagerProtocol> profileManager;
@property (nonatomic) OWSPrimaryStorage *primaryStorage;
@property (nonatomic) OWSIdentityManager *identityManager;
@property (nonatomic) TSAccountManager *tsAccountManager;
@property (nonatomic) OWSDisappearingMessagesJob *disappearingMessagesJob;
@property (nonatomic) OWSReadReceiptManager *readReceiptManager;
@ -36,7 +35,6 @@ static SSKEnvironment *sharedSSKEnvironment;
- (instancetype)initWithProfileManager:(id<ProfileManagerProtocol>)profileManager
primaryStorage:(OWSPrimaryStorage *)primaryStorage
identityManager:(OWSIdentityManager *)identityManager
tsAccountManager:(TSAccountManager *)tsAccountManager
disappearingMessagesJob:(OWSDisappearingMessagesJob *)disappearingMessagesJob
readReceiptManager:(OWSReadReceiptManager *)readReceiptManager
@ -52,7 +50,6 @@ static SSKEnvironment *sharedSSKEnvironment;
_profileManager = profileManager;
_primaryStorage = primaryStorage;
_identityManager = identityManager;
_tsAccountManager = tsAccountManager;
_disappearingMessagesJob = disappearingMessagesJob;
_readReceiptManager = readReceiptManager;

View File

@ -28,7 +28,7 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
}
let senderPublicKey = incomingMessage.authorId
let userPublicKey = GeneralUtilities.getUserPublicKey()
let userPublicKey = getUserHexEncodedPublicKey()
guard senderPublicKey != userPublicKey else {
// Ignore PNs for messages sent by the current user
// after handling the message. Otherwise the closed

View File

@ -2,8 +2,6 @@ import Foundation
import SessionUtilitiesKit
public struct SNSnodeKitConfiguration {
public let storage: SessionSnodeKitStorageProtocol
internal static var shared: SNSnodeKitConfiguration!
}
@ -22,7 +20,6 @@ public enum SNSnodeKit { // Just to make the external API nice
)
}
public static func configure(storage: SessionSnodeKitStorageProtocol) {
SNSnodeKitConfiguration.shared = SNSnodeKitConfiguration(storage: storage)
public static func configure() {
}
}

View File

@ -6,12 +6,12 @@ public enum Legacy {
// MARK: - Collections and Keys
internal static let swarmCollectionPrefix = "LokiSwarmCollection-"
internal static let lastSnodePoolRefreshDateKey = "lastSnodePoolRefreshDate"
internal static let snodePoolCollection = "LokiSnodePoolCollection"
internal static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
internal static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection"
internal static let lastMessageHashCollection = "LokiLastMessageHashCollection" // TODO: Remove this one? (make it a query??)
internal static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
// TODO: - "lastSnodePoolRefreshDate"
// MARK: - Types
@ -29,15 +29,6 @@ public enum Legacy {
}
// MARK: Nested Types
public enum Method : String {
case getSwarm = "get_snodes_for_pubkey"
case getMessages = "retrieve"
case sendMessage = "store"
case deleteMessage = "delete"
case oxenDaemonRPCCall = "oxend_request"
case getInfo = "info"
case clearAllData = "delete_all"
}
public struct KeySet {
public let ed25519Key: String

View File

@ -14,8 +14,15 @@ enum _002_YDBToGRDBMigration: Migration {
// Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult'
var snodeResult: Set<Legacy.Snode> = []
var snodeSetResult: [String: Set<Legacy.Snode>] = [:]
var lastSnodePoolRefreshDate: Date? = nil
Storage.read { transaction in
// Process the lastSnodePoolRefreshDate
lastSnodePoolRefreshDate = transaction.object(
forKey: Legacy.lastSnodePoolRefreshDateKey,
inCollection: Legacy.lastSnodePoolRefreshDateCollection
) as? Date
// Process the OnionRequestPaths
if
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode,
@ -65,6 +72,10 @@ enum _002_YDBToGRDBMigration: Migration {
}
}
// Insert the data into GRDB
db[.lastSnodePoolRefreshDate] = lastSnodePoolRefreshDate
try snodeResult.forEach { legacySnode in
try Snode(
address: legacySnode.address,
@ -79,20 +90,13 @@ enum _002_YDBToGRDBMigration: Migration {
// Note: In this case the 'nodeIndex' is irrelivant
try SnodeSet(
key: key,
nodeIndex: UInt(nodeIndex),
nodeIndex: nodeIndex,
address: legacySnode.address,
port: legacySnode.port
).insert(db)
}
}
// TODO: This
// public func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any) {
// (transaction as! YapDatabaseReadWriteTransaction).setObject(date, forKey: "lastSnodePoolRefreshDate", inCollection: Storage.lastSnodePoolRefreshDateCollection)
// }
print("RAWR")
// MARK: - Received Messages & Last Message Hash
var lastMessageResults: [String: (hash: String, json: JSON)] = [:]

View File

@ -4,18 +4,100 @@ import Foundation
import GRDB
import SessionUtilitiesKit
public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable {
public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable, CustomStringConvertible {
public static var databaseTableName: String { "snode" }
static let snodeSet = hasMany(SnodeSet.self)
static let snodeSetForeignKey = ForeignKey(
[Columns.address, Columns.port],
to: [SnodeSet.Columns.address, SnodeSet.Columns.port]
)
public enum Columns: String, CodingKey, ColumnExpression {
case address
case port
case ed25519PublicKey
case x25519PublicKey
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case address = "public_ip"
case port = "storage_port"
case ed25519PublicKey = "pubkey_ed25519"
case x25519PublicKey = "pubkey_x25519"
}
public let address: String
public let port: UInt16
public let ed25519PublicKey: String
public let x25519PublicKey: String
public var ip: String {
guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else {
return address
}
return String(address[range.upperBound..<address.endIndex])
}
let address: String
let port: UInt16
let ed25519PublicKey: String
let x25519PublicKey: String
public var snodeSet: QueryInterfaceRequest<SnodeSet> {
request(for: Snode.snodeSet)
}
public var description: String { return "\(address):\(port)" }
}
// MARK: - Decoder
extension Snode {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
do {
let address: String = try container.decode(String.self, forKey: .address)
guard address != "0.0.0.0" else { throw SnodeAPI.Error.invalidIP }
self = Snode(
address: (address.starts(with: "https://") ? address : "https://\(address)"),
port: try container.decode(UInt16.self, forKey: .port),
ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey),
x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey)
)
}
catch {
SNLog("Failed to parse snode: \(error.localizedDescription).")
throw HTTP.Error.invalidJSON
}
}
}
// MARK: - Convenience
internal extension Snode {
static func fetchSet(_ db: Database, publicKey: String) throws -> Set<Snode> {
return try Snode
.joining(
required: Snode.snodeSet
.filter(SnodeSet.Columns.key == publicKey)
.order(SnodeSet.Columns.nodeIndex)
)
.fetchSet(db)
}
}
internal extension Collection where Element == Snode {
func save(_ db: Database, key: String) throws {
try self.enumerated().forEach { nodeIndex, node in
try node.save(db)
try SnodeSet(
key: key,
nodeIndex: nodeIndex,
address: node.address,
port: node.port
).save(db)
}
}
}
internal extension Collection where Element == [Snode] {
func save(_ db: Database) throws {
try self.enumerated().forEach { pathIndex, path in
try path.save(db, key: "\(SnodeSet.onionRequestPathPrefix)\(pathIndex)")
}
}
}

View File

@ -4,16 +4,56 @@ import Foundation
import GRDB
import SessionUtilitiesKit
struct SnodeReceivedMessageInfo: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
static var databaseTableName: String { "snodeReceivedMessageInfo" }
public struct SnodeReceivedMessageInfo: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "snodeReceivedMessageInfo" }
public enum Columns: String, CodingKey, ColumnExpression {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case key
case hash
case expirationDateMs
}
let key: String
let hash: String
let expirationDateMs: Int64
public let key: String
public let hash: String
public let expirationDateMs: Int64
}
// MARK: - Convenience
public extension SnodeReceivedMessageInfo {
private static func key(for snode: Snode, publicKey: String) -> String {
return "\(snode.address):\(snode.port).\(publicKey)"
}
init(
snode: Snode,
publicKey: String,
hash: String,
expirationDateMs: Int64?
) {
self.key = SnodeReceivedMessageInfo.key(for: snode, publicKey: publicKey)
self.hash = hash
self.expirationDateMs = (expirationDateMs ?? 0)
}
static func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String) {
// Clear out the 'expirationDateMs' value for all expired (but non-0) message infos
GRDBStorage.shared.write { db in
try? SnodeReceivedMessageInfo
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey))
.filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > 0)
.updateAll(db, SnodeReceivedMessageInfo.Columns.expirationDateMs.set(to: 0))
}
}
static func fetchLastNotExpired(for snode: Snode, associatedWith publicKey: String) -> SnodeReceivedMessageInfo? {
return GRDBStorage.shared.read { db in
try? SnodeReceivedMessageInfo
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey))
.order(SnodeReceivedMessageInfo.Columns.expirationDateMs)
.reversed()
.fetchOne(db)
}
}
}

View File

@ -4,24 +4,59 @@ import Foundation
import GRDB
import SessionUtilitiesKit
struct SnodeSet: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
static var databaseTableName: String { "snodeSet" }
static let nodes = hasMany(Snode.self)
static let onionRequestPathPrefix = "OnionRequestPath-"
public enum Columns: String, CodingKey, ColumnExpression {
public struct SnodeSet: Codable, FetchableRecord, EncodableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static let onionRequestPathPrefix = "OnionRequestPath-"
public static var databaseTableName: String { "snodeSetAssociation" }
static let node = hasOne(Snode.self, using: Snode.snodeSetForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case key
case nodeIndex
case address
case port
}
let key: String
let nodeIndex: UInt
let address: String
let port: UInt16
public let key: String
public let nodeIndex: Int
public let address: String
public let port: UInt16
var nodes: QueryInterfaceRequest<Snode> {
request(for: SnodeSet.nodes)
public var node: QueryInterfaceRequest<Snode> {
request(for: SnodeSet.node)
}
}
// MARK: - Convenience
internal extension SnodeSet {
static func fetchAllOnionRequestPaths(_ db: Database) throws -> [[Snode]] {
struct ResultWrapper: Decodable, FetchableRecord {
let key: String
let nodeIndex: Int
let address: String
let port: UInt16
let snode: Snode
}
return try SnodeSet
.filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%"))
.order(SnodeSet.Columns.nodeIndex)
.order(SnodeSet.Columns.key)
.including(required: SnodeSet.node)
.asRequest(of: ResultWrapper.self)
.fetchAll(db)
.reduce(into: [:]) { prev, next in // Reducing will lose the 'key' sorting
prev[next.key] = (prev[next.key] ?? []).appending(next.snode)
}
.asArray()
.sorted(by: { lhs, rhs in lhs.key < rhs.key })
.compactMap { _, nodes in !nodes.isEmpty ? nodes : nil } // Exclude empty sets
}
static func clearOnionRequestPaths(_ db: Database) throws {
try SnodeSet
.filter(SnodeSet.Columns.key.like("\(SnodeSet.onionRequestPathPrefix)%"))
.deleteAll(db)
}
}

View File

@ -1,3 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
extension Setting.DateKey {
static let lastSnodePoolRefreshDate: Setting.DateKey = "lastSnodePoolRefreshDate"
}

View File

@ -0,0 +1,33 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
public struct SnodeReceivedMessage: CustomDebugStringConvertible {
public let info: SnodeReceivedMessageInfo
public let data: Data
init?(snode: Snode, publicKey: String, rawMessage: JSON) {
guard let hash: String = rawMessage["hash"] as? String else { return nil }
guard
let base64EncodedString: String = rawMessage["data"] as? String,
let data: Data = Data(base64Encoded: base64EncodedString)
else {
SNLog("Failed to decode data for message: \(rawMessage).")
return nil
}
self.info = SnodeReceivedMessageInfo(
snode: snode,
publicKey: publicKey,
hash: hash,
expirationDateMs: rawMessage["expiration"] as? Int64
)
self.data = data
}
public var debugDescription: String {
return "{\"hash\":\(info.hash),\"expiration\":\(info.expirationDateMs),\"data\":\"\(data.base64EncodedString())\"}"
}
}

View File

@ -0,0 +1,58 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
/// It looks like the structure for the service node returned from `get_snodes_for_pubkey` is different from
/// the usual structure, this type is used as an intemediary to convert to the usual 'Snode' type
// FIXME: Hopefully at some point this different Snode structure will be deprecated and can be removed
internal struct SwarmSnode: Codable {
public enum CodingKeys: String, CodingKey {
case address = "ip"
case port
case ed25519PublicKey = "pubkey_ed25519"
case x25519PublicKey = "pubkey_x25519"
}
let address: String
let port: UInt16
let ed25519PublicKey: String
let x25519PublicKey: String
}
// MARK: - Convenience
extension SwarmSnode {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
do {
let address: String = try container.decode(String.self, forKey: .address)
let portString: String = try container.decode(String.self, forKey: .port)
guard address != "0.0.0.0", let port: UInt16 = UInt16(portString) else {
throw SnodeAPI.Error.invalidIP
}
self = SwarmSnode(
address: (address.starts(with: "https://") ? address : "https://\(address)"),
port: port,
ed25519PublicKey: try container.decode(String.self, forKey: .ed25519PublicKey),
x25519PublicKey: try container.decode(String.self, forKey: .x25519PublicKey)
)
}
catch {
SNLog("Failed to parse snode: \(error.localizedDescription).")
throw HTTP.Error.invalidJSON
}
}
func toSnode() -> Snode {
return Snode(
address: address,
port: port,
ed25519PublicKey: ed25519PublicKey,
x25519PublicKey: x25519PublicKey
)
}
}

View File

@ -22,7 +22,7 @@ internal extension OnionRequestAPI {
// Wrapping isn't needed for file server or open group onion requests
switch destination {
case .snode(let snode):
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
let snodeX25519PublicKey = snode.x25519PublicKey
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
let result = try AESGCM.encrypt(plaintext, for: snodeX25519PublicKey)
@ -46,7 +46,7 @@ internal extension OnionRequestAPI {
var parameters: JSON
switch rhs {
case .snode(let snode):
let snodeED25519PublicKey = snode.publicKeySet.ed25519Key
let snodeED25519PublicKey = snode.ed25519PublicKey
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, let target, _, let scheme, let port):
let scheme = scheme ?? "https"
@ -57,7 +57,7 @@ internal extension OnionRequestAPI {
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
let snodeX25519PublicKey = snode.x25519PublicKey
x25519PublicKey = snodeX25519PublicKey
case .server(_, _, let serverX25519PublicKey, _, _):
x25519PublicKey = serverX25519PublicKey

View File

@ -1,19 +1,40 @@
import Foundation
import CryptoSwift
import GRDB
import PromiseKit
import SessionUtilitiesKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI {
private static var buildPathsPromise: Promise<[Path]>? = nil
private static var buildPathsPromise: Promise<[[Snode]]>? = nil
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var pathFailureCount: [Path:UInt] = [:]
private static var pathFailureCount: [[Snode]: UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var snodeFailureCount: [Legacy.Snode:UInt] = [:]
private static var snodeFailureCount: [Snode: UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var guardSnodes: Set<Legacy.Snode> = []
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
public static var guardSnodes: Set<Snode> = []
// Not a set to ensure we consistently show the same path to the user
private static var _paths: [[Snode]]?
public static var paths: [[Snode]] {
get {
if let paths: [[Snode]] = _paths { return paths }
let results: [[Snode]]? = GRDBStorage.shared.read { db in
try? SnodeSet.fetchAllOnionRequestPaths(db)
}
if results?.isEmpty == false { _paths = results }
return (results ?? [])
}
set { _paths = newValue }
}
// MARK: Settings
// MARK: - Settings
public static let maxRequestSize = 10_000_000 // 10 MB
/// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 3
@ -27,97 +48,80 @@ public enum OnionRequestAPI {
/// The number of guard snodes required to maintain `targetPathCount` paths.
private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path
// MARK: Destination
public enum Destination : CustomStringConvertible {
case snode(Legacy.Snode)
case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?)
public var description: String {
switch self {
case .snode(let snode): return "Service node \(snode.ip):\(snode.port)"
case .server(let host, _, _, _, _): return host
}
}
}
// MARK: Error
public enum Error : LocalizedError {
case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: Destination)
case insufficientSnodes
case invalidURL
case missingSnodeVersion
case snodePublicKeySetMissing
case unsupportedSnodeVersion(String)
public var errorDescription: String? {
switch self {
case .httpRequestFailedAtDestination(let statusCode, _, let destination):
if statusCode == 429 {
return "Rate limited."
} else {
return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)."
}
case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path."
case .invalidURL: return "Invalid URL"
case .missingSnodeVersion: return "Missing Service Node version."
case .snodePublicKeySetMissing: return "Missing Service Node public key set."
case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)."
}
}
}
// MARK: Path
public typealias Path = [Legacy.Snode]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: Legacy.Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data)
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data)
// MARK: Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
private static func testSnode(_ snode: Legacy.Snode) -> Promise<Void> {
private static func testSnode(_ snode: Snode) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
HTTP.execute(.get, url, timeout: timeout).done2 { json in
guard let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
if version >= "2.0.7" {
seal.fulfill(())
} else {
SNLog("Unsupported snode version: \(version).")
seal.reject(Error.unsupportedSnodeVersion(version))
HTTP.execute(.get, url, timeout: timeout)
.done2 { json in
guard let version = json["version"] as? String else {
return seal.reject(Error.missingSnodeVersion)
}
if version >= "2.0.7" {
seal.fulfill(())
}
else {
SNLog("Unsupported snode version: \(version).")
seal.reject(Error.unsupportedSnodeVersion(version))
}
}
.catch2 { error in
seal.reject(error)
}
}.catch2 { error in
seal.reject(error)
}
}
return promise
}
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
private static func getGuardSnodes(reusing reusableGuardSnodes: [Legacy.Snode]) -> Promise<Set<Legacy.Snode>> {
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Legacy.Snode>> { $0.fulfill(guardSnodes) }
} else {
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
}
else {
SNLog("Populating guard snode cache.")
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
// Sync on LokiAPI.workQueue
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes)
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: Error.insufficientSnodes) }
func getGuardSnode() -> Promise<Legacy.Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<Legacy.Snode> { $0.reject(Error.insufficientSnodes) } }
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else {
return Promise(error: Error.insufficientSnodes)
}
func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which
// is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else {
return Promise<Snode> { $0.reject(Error.insufficientSnodes) }
}
unusedSnodes.remove(candidate) // All used snodes should be unique
SNLog("Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() }
}
}
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in
getGuardSnode()
}
return when(fulfilled: promises).map2 { guardSnodes in
let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
OnionRequestAPI.guardSnodes = guardSnodesAsSet
return guardSnodesAsSet
}
}
@ -126,40 +130,50 @@ public enum OnionRequestAPI {
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
@discardableResult
private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
private static func buildPaths(reusing reusablePaths: [[Snode]]) -> Promise<[[Snode]]> {
if let existingBuildPathsPromise = buildPathsPromise { return existingBuildPathsPromise }
SNLog("Building onion request paths.")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
let reusableGuardSnodes = reusablePaths.map { $0[0] }
let promise: Promise<[Path]> = getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in
var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
// randomElement() uses the system's default random generator, which is cryptographically secure
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
let promise: Promise<[[Snode]]> = getGuardSnodes(reusing: reusableGuardSnodes)
.map2 { guardSnodes -> [[Snode]] in
var unusedSnodes = SnodeAPI.snodePool
.subtracting(guardSnodes)
.subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
// randomElement() uses the system's default random generator, which is cryptographically secure
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
}
SNLog("Built new onion request path: \(result.prettifiedDescription).")
return result
}
SNLog("Built new onion request path: \(result.prettifiedDescription).")
return result
}
}.map2 { paths in
OnionRequestAPI.paths = paths + reusablePaths
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNLog("Persisting onion request paths to database.")
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction)
.map2 { paths in
OnionRequestAPI.paths = paths + reusablePaths
GRDBStorage.shared.write { db in
SNLog("Persisting onion request paths to database.")
try? paths.save(db)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
}
promise.done2 { _ in buildPathsPromise = nil }
promise.catch2 { _ in buildPathsPromise = nil }
buildPathsPromise = promise
@ -167,63 +181,68 @@ public enum OnionRequestAPI {
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
private static func getPath(excluding snode: Legacy.Snode?) -> Promise<Path> {
private static func getPath(excluding snode: Snode?) -> Promise<[Snode]> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
var paths = OnionRequestAPI.paths
if paths.isEmpty {
paths = SNSnodeKitConfiguration.shared.storage.getOnionRequestPaths()
OnionRequestAPI.paths = paths
if !paths.isEmpty {
guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= 2 {
guardSnodes.formUnion([ paths[1][0] ])
}
let paths: [[Snode]] = OnionRequestAPI.paths
if !paths.isEmpty {
guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= 2 {
guardSnodes.formUnion([ paths[1][0] ])
}
}
// randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= targetPathCount {
if let snode = snode {
if let snode: Snode = snode {
return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) }
} else {
}
else {
return Promise { $0.fulfill(paths.randomElement()!) }
}
} else if !paths.isEmpty {
}
else if !paths.isEmpty {
if let snode = snode {
if let path = paths.first(where: { !$0.contains(snode) }) {
buildPaths(reusing: paths) // Re-build paths in the background
return Promise { $0.fulfill(path) }
} else {
}
else {
return buildPaths(reusing: paths).map2 { paths in
return paths.filter { !$0.contains(snode) }.randomElement()!
}
}
} else {
}
else {
buildPaths(reusing: paths) // Re-build paths in the background
return Promise { $0.fulfill(paths.randomElement()!) }
}
} else {
}
else {
return buildPaths(reusing: []).map2 { paths in
if let snode = snode {
if let path = paths.filter({ !$0.contains(snode) }).randomElement() {
return path
} else {
throw Error.insufficientSnodes
}
} else {
return paths.randomElement()!
throw Error.insufficientSnodes
}
return paths.randomElement()!
}
}
}
private static func dropGuardSnode(_ snode: Legacy.Snode) {
private static func dropGuardSnode(_ snode: Snode) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
guardSnodes = guardSnodes.filter { $0 != snode }
}
private static func drop(_ snode: Legacy.Snode) throws {
private static func drop(_ snode: Snode) throws {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -244,13 +263,14 @@ public enum OnionRequestAPI {
oldPaths.remove(at: pathIndex)
let newPaths = oldPaths + [ path ]
paths = newPaths
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
GRDBStorage.shared.write { db in
SNLog("Persisting onion request paths to database.")
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: newPaths, using: transaction)
try? newPaths.save(db)
}
}
private static func drop(_ path: Path) {
private static func drop(_ path: [Snode]) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -259,56 +279,69 @@ public enum OnionRequestAPI {
guard let pathIndex = paths.firstIndex(of: path) else { return }
paths.remove(at: pathIndex)
OnionRequestAPI.paths = paths
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
if !paths.isEmpty {
SNLog("Persisting onion request paths to database.")
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: paths, using: transaction)
} else {
GRDBStorage.shared.write { db in
guard !paths.isEmpty else {
SNLog("Clearing onion request paths.")
SNSnodeKitConfiguration.shared.storage.setOnionRequestPaths(to: [], using: transaction)
try? SnodeSet.clearOnionRequestPaths(db)
return
}
SNLog("Persisting onion request paths to database.")
try? paths.save(db)
}
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
var guardSnode: Legacy.Snode!
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult!
var snodeToExclude: Legacy.Snode?
var snodeToExclude: Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination).then2 { r -> Promise<AESGCM.EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = destination
func addLayer() -> Promise<AESGCM.EncryptionResult> {
if path.isEmpty {
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = Destination.snode(path.removeLast())
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<AESGCM.EncryptionResult> in
encryptionResult = r
rhs = lhs
return addLayer()
return getPath(excluding: snodeToExclude)
.then2 { path -> Promise<AESGCM.EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination)
.then2 { r -> Promise<AESGCM.EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = destination
func addLayer() -> Promise<AESGCM.EncryptionResult> {
guard !path.isEmpty else {
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
}
let lhs = Destination.snode(path.removeLast())
return OnionRequestAPI
.encryptHop(from: lhs, to: rhs, using: encryptionResult)
.then2 { r -> Promise<AESGCM.EncryptionResult> in
encryptionResult = r
rhs = lhs
return addLayer()
}
}
return addLayer()
}
}
return addLayer()
}
}.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
}
// MARK: Public API
/// Sends an onion request to `snode`. Builds new paths as needed.
public static func sendOnionRequest(to snode: Legacy.Snode, invoking method: Legacy.Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<JSON> {
public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPI.Endpoint, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<JSON> {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<JSON> 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
}
}
@ -365,7 +398,7 @@ public enum OnionRequestAPI {
public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Legacy.Snode?
var guardSnode: Snode?
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode
@ -442,7 +475,7 @@ public enum OnionRequestAPI {
let prefix = "Next node not found: "
if let message = json?["result"] as? String, message.hasPrefix(prefix) {
let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..<message.endIndex]
if let path = path, let snode = path.first(where: { $0.publicKeySet.ed25519Key == ed25519PublicKey }) {
if let path = path, let snode = path.first(where: { $0.ed25519PublicKey == ed25519PublicKey }) {
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?? 0
snodeFailureCount += 1
if snodeFailureCount >= snodeFailureThreshold {

View File

@ -1,6 +1,7 @@
import PromiseKit
import SessionUtilitiesKit
import Sodium
import GRDB
import SessionUtilitiesKit
@objc(SNSnodeAPI)
public final class SnodeAPI : NSObject {
@ -8,12 +9,12 @@ public final class SnodeAPI : NSObject {
private static var hasLoadedSnodePool = false
private static var loadedSwarms: Set<String> = []
private static var getSnodePoolPromise: Promise<Set<Legacy.Snode>>?
private static var getSnodePoolPromise: Promise<Set<Snode>>?
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
internal static var snodeFailureCount: [Legacy.Snode:UInt] = [:]
internal static var snodeFailureCount: [Snode: UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
internal static var snodePool: Set<Legacy.Snode> = []
internal static var snodePool: Set<Snode> = []
/// The offset between the user's clock and the Service Node's clock. Used in cases where the
/// user's clock is incorrect.
@ -21,7 +22,7 @@ public final class SnodeAPI : NSObject {
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var clockOffset: Int64 = 0
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var swarmCache: [String:Set<Legacy.Snode>] = [:]
public static var swarmCache: [String: Set<Snode>] = [:]
// MARK: Settings
private static let maxRetryCount: UInt = 8
@ -30,35 +31,6 @@ public final class SnodeAPI : NSObject {
private static let snodeFailureThreshold = 3
private static let targetSwarmSnodeCount = 2
private static let minSnodePoolCount = 12
// MARK: Error
public enum Error : LocalizedError {
case generic
case clockOutOfSync
case snodePoolUpdatingFailed
case inconsistentSnodePools
case noKeyPair
case signingFailed
// ONS
case decryptionFailed
case hashingFailed
case validationFailed
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time."
case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool."
case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network."
case .noKeyPair: return "Missing user key pair."
case .signingFailed: return "Couldn't sign message."
// ONS
case .decryptionFailed: return "Couldn't decrypt ONS name."
case .hashingFailed: return "Couldn't compute ONS name hash."
case .validationFailed: return "ONS name validation failed."
}
}
}
// MARK: Type Aliases
public typealias MessageListPromise = Promise<[JSON]>
@ -68,23 +40,30 @@ public final class SnodeAPI : NSObject {
// MARK: Snode Pool Interaction
private static func loadSnodePoolIfNeeded() {
guard !hasLoadedSnodePool else { return }
snodePool = SNSnodeKitConfiguration.shared.storage.getSnodePool()
GRDBStorage.shared.read { db in
snodePool = ((try? Snode.fetchSet(db)) ?? Set())
}
hasLoadedSnodePool = true
}
private static func setSnodePool(to newValue: Set<Legacy.Snode>, using transaction: Any? = nil) {
private static func setSnodePool(to newValue: Set<Snode>, db: Database? = nil) {
snodePool = newValue
let storage = SNSnodeKitConfiguration.shared.storage
if let transaction = transaction {
storage.setSnodePool(to: newValue, using: transaction)
} else {
storage.writeSync { transaction in
storage.setSnodePool(to: newValue, using: transaction)
if let db: Database = db {
_ = try? Snode.deleteAll(db)
newValue.forEach { try? $0.save(db) }
}
else {
GRDBStorage.shared.write { db in
_ = try? Snode.deleteAll(db)
newValue.forEach { try? $0.save(db) }
}
}
}
private static func dropSnodeFromSnodePool(_ snode: Legacy.Snode) {
private static func dropSnodeFromSnodePool(_ snode: Snode) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -95,6 +74,7 @@ public final class SnodeAPI : NSObject {
@objc public static func clearSnodePool() {
snodePool.removeAll()
Threading.workQueue.async {
setSnodePool(to: [])
}
@ -103,22 +83,27 @@ public final class SnodeAPI : NSObject {
// MARK: Swarm Interaction
private static func loadSwarmIfNeeded(for publicKey: String) {
guard !loadedSwarms.contains(publicKey) else { return }
swarmCache[publicKey] = SNSnodeKitConfiguration.shared.storage.getSwarm(for: publicKey)
GRDBStorage.shared.read { db in
swarmCache[publicKey] = ((try? Snode.fetchSet(db, publicKey: publicKey)) ?? [])
}
loadedSwarms.insert(publicKey)
}
private static func setSwarm(to newValue: Set<Legacy.Snode>, for publicKey: String, persist: Bool = true) {
private static func setSwarm(to newValue: Set<Snode>, for publicKey: String, persist: Bool = true) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
swarmCache[publicKey] = newValue
guard persist else { return }
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setSwarm(to: newValue, for: publicKey, using: transaction)
GRDBStorage.shared.write { db in
try? newValue.save(db, key: publicKey)
}
}
public static func dropSnodeFromSwarmIfNeeded(_ snode: Legacy.Snode, publicKey: String) {
public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -129,19 +114,21 @@ public final class SnodeAPI : NSObject {
}
// MARK: Internal API
internal static func invoke(_ method: Legacy.Snode.Method, on snode: Legacy.Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
internal static func invoke(_ method: Endpoint, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
if Features.useOnionRequests {
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
} else {
}
else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error }
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
}
}
}
private static func getNetworkTime(from snode: Legacy.Snode) -> Promise<UInt64> {
private static func getNetworkTime(from snode: Snode) -> Promise<UInt64> {
return invoke(.getInfo, on: snode, parameters: [:]).map2 { rawResponse in
guard let json = rawResponse as? JSON,
let timestamp = json["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON }
@ -149,140 +136,165 @@ public final class SnodeAPI : NSObject {
}
}
internal static func getRandomSnode() -> Promise<Legacy.Snode> {
internal static func getRandomSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
return getSnodePool().map2 { $0.randomElement()! }
}
private static func getSnodePoolFromSeedNode() -> Promise<Set<Legacy.Snode>> {
private static func getSnodePoolFromSeedNode() -> Promise<Set<Snode>> {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
"method" : "get_n_service_nodes",
"params" : [
"active_only" : true,
"limit" : 256,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
"method": "get_n_service_nodes",
"params": [
"active_only": true,
"limit": 256,
"fields": [
"public_ip": true,
"storage_port": true,
"pubkey_ed25519": true,
"pubkey_x25519": true
]
]
]
SNLog("Populating snode pool using seed node: \(target).")
let (promise, seal) = Promise<Set<Legacy.Snode>>.pending()
let (promise, seal) = Promise<Set<Snode>>.pending()
Threading.workQueue.async {
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Set<Legacy.Snode> in
guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw Error.snodePoolUpdatingFailed }
return Set(rawSnodes.compactMap { rawSnode in
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true)
.map2 { json -> Set<Snode> in
guard
let intermediate = json["result"] as? JSON,
let rawSnodes = intermediate["service_node_states"] as? [JSON],
let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: [])
else {
throw Error.snodePoolUpdatingFailed
}
return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
}
}.done2 { snodePool in
return ((try? JSONDecoder().decode([Failable<Snode>].self, from: snodeData)) ?? [])
.compactMap { $0.value }
.asSet()
}
}
.done2 { snodePool in
SNLog("Got snode pool from seed node: \(target).")
seal.fulfill(snodePool)
}.catch2 { error in
}
.catch2 { error in
SNLog("Failed to contact seed node at: \(target).")
seal.reject(error)
}
}
return promise
}
private static func getSnodePoolFromSnode() -> Promise<Set<Legacy.Snode>> {
private static func getSnodePoolFromSnode() -> Promise<Set<Snode>> {
var snodePool = SnodeAPI.snodePool
var snodes: Set<Legacy.Snode> = []
var snodes: Set<Snode> = []
(0..<3).forEach { _ in
let snode = snodePool.randomElement()!
guard let snode = snodePool.randomElement() else { return }
snodePool.remove(snode)
snodes.insert(snode)
}
let snodePoolPromises: [Promise<Set<Legacy.Snode>>] = snodes.map { snode in
let snodePoolPromises: [Promise<Set<Snode>>] = snodes.map { snode in
return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
// Don't specify a limit in the request. Service nodes return a shuffled
// list of nodes so if we specify a limit the 3 responses we get might have
// very little overlap.
let parameters: JSON = [
"endpoint" : "get_service_nodes",
"params" : [
"active_only" : true,
"fields" : [
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
"endpoint": "get_service_nodes",
"params": [
"active_only": true,
"fields": [
"public_ip": true,
"storage_port": true,
"pubkey_ed25519": true,
"pubkey_x25519": true
]
]
]
return invoke(.oxenDaemonRPCCall, on: snode, parameters: parameters).map2 { rawResponse in
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON,
let rawSnodes = intermediate["service_node_states"] as? [JSON] else {
guard
let json = rawResponse as? JSON,
let intermediate = json["result"] as? JSON,
let rawSnodes = intermediate["service_node_states"] as? [JSON],
let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: [])
else {
throw Error.snodePoolUpdatingFailed
}
return Set(rawSnodes.compactMap { rawSnode in
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
}
return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
return ((try? JSONDecoder().decode([Failable<Snode>].self, from: snodeData)) ?? [])
.compactMap { $0.value }
.asSet()
}
}
}
let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Legacy.Snode> in
var result: Set<Legacy.Snode> = results[0]
results.forEach { result = result.intersection($0) }
if result.count > 24 { // We want the snodes to agree on at least this many snodes
// Limit the snode pool size to 256 so that we don't go too long without
// refreshing it
return (result.count > 256) ? Set([Legacy.Snode](result)[0..<256]) : result
} else {
throw Error.inconsistentSnodePools
}
let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Snode> in
let result: Set<Snode> = results.reduce(Set()) { prev, next in prev.intersection(next) }
// We want the snodes to agree on at least this many snodes
guard result.count > 24 else { throw Error.inconsistentSnodePools }
// Limit the snode pool size to 256 so that we don't go too long without
// refreshing it
return Set(result.prefix(256))
}
return promise
}
// MARK: Public API
@objc(getSnodePool)
public static func objc_getSnodePool() -> AnyPromise {
AnyPromise.from(getSnodePool())
}
public static func getSnodePool() -> Promise<Set<Legacy.Snode>> {
public static func getSnodePool() -> Promise<Set<Snode>> {
loadSnodePoolIfNeeded()
let now = Date()
let hasSnodePoolExpired = given(Storage.shared.getLastSnodePoolRefreshDate()) { now.timeIntervalSince($0) > 2 * 60 * 60 } ?? true
let hasSnodePoolExpired = given(GRDBStorage.shared[.lastSnodePoolRefreshDate]) { now.timeIntervalSince($0) > 2 * 60 * 60 } ?? true
let snodePool = SnodeAPI.snodePool
let hasInsufficientSnodes = (snodePool.count < minSnodePoolCount)
if hasInsufficientSnodes || hasSnodePoolExpired {
if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise }
let promise: Promise<Set<Legacy.Snode>>
let promise: Promise<Set<Snode>>
if snodePool.count < minSnodePoolCount {
promise = getSnodePoolFromSeedNode()
} else {
}
else {
promise = getSnodePoolFromSnode().recover2 { _ in
getSnodePoolFromSeedNode()
}
}
getSnodePoolPromise = promise
promise.map2 { snodePool -> Set<Legacy.Snode> in
if snodePool.isEmpty {
throw Error.snodePoolUpdatingFailed
} else {
return snodePool
}
promise.map2 { snodePool -> Set<Snode> in
guard !snodePool.isEmpty else { throw Error.snodePoolUpdatingFailed }
return snodePool
}
promise.then2 { snodePool -> Promise<Set<Legacy.Snode>> in
let (promise, seal) = Promise<Set<Legacy.Snode>>.pending()
SNSnodeKitConfiguration.shared.storage.write(with: { transaction in
Storage.shared.setLastSnodePoolRefreshDate(to: now, using: transaction)
setSnodePool(to: snodePool, using: transaction)
}, completion: {
seal.fulfill(snodePool)
})
promise.then2 { snodePool -> Promise<Set<Snode>> in
let (promise, seal) = Promise<Set<Snode>>.pending()
GRDBStorage.shared.writeAsync(
updates: { db in
db[.lastSnodePoolRefreshDate] = now
setSnodePool(to: snodePool, db: db)
},
completion: { _, _ in
seal.fulfill(snodePool)
}
)
return promise
}
promise.done2 { _ in
@ -291,10 +303,11 @@ public final class SnodeAPI : NSObject {
promise.catch2 { _ in
getSnodePoolPromise = nil
}
return promise
} else {
return Promise.value(snodePool)
}
return Promise.value(snodePool)
}
public static func getSessionID(for onsName: String) -> Promise<String> {
@ -366,49 +379,55 @@ public final class SnodeAPI : NSObject {
return promise
}
public static func getTargetSnodes(for publicKey: String) -> Promise<[Legacy.Snode]> {
public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
// shuffled() uses the system's default random generator, which is cryptographically secure
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
}
public static func getSwarm(for publicKey: String) -> Promise<Set<Legacy.Snode>> {
public static func getSwarm(for publicKey: String) -> Promise<Set<Snode>> {
loadSwarmIfNeeded(for: publicKey)
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minSwarmSnodeCount {
return Promise<Set<Legacy.Snode>> { $0.fulfill(cachedSwarm) }
} else {
SNLog("Getting swarm for: \((publicKey == SNSnodeKitConfiguration.shared.storage.getUserPublicKey()) ? "self" : publicKey).")
let parameters: [String:Any] = [ "pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey ]
return getRandomSnode().then2 { snode in
return Promise<Set<Snode>> { $0.fulfill(cachedSwarm) }
}
SNLog("Getting swarm for: \((publicKey == getUserHexEncodedPublicKey()) ? "self" : publicKey).")
let parameters: [String: Any] = [
"pubKey": Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey
]
return getRandomSnode()
.then2 { snode in
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
}
}.map2 { rawSnodes in
}
.map2 { rawSnodes in
let swarm = parseSnodes(from: rawSnodes)
setSwarm(to: swarm, for: publicKey)
return swarm
}
}
}
public static func getRawMessages(from snode: Legacy.Snode, associatedWith publicKey: String) -> RawResponsePromise {
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
let (promise, seal) = RawResponsePromise.pending()
Threading.workQueue.async {
getMessagesInternal(from: snode, associatedWith: publicKey).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
getMessagesInternal(from: snode, associatedWith: publicKey)
.done2 { seal.fulfill($0) }
.catch2 { seal.reject($0) }
}
return promise
}
private static func getMessagesInternal(from snode: Legacy.Snode, associatedWith publicKey: String) -> RawResponsePromise {
let storage = SNSnodeKitConfiguration.shared.storage
private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
// NOTE: All authentication logic is currently commented out, the reason being that we can't currently support
// it yet for closed groups. The Storage Server requires an ed25519 key pair, but we don't have that for our
// closed groups.
// guard let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
// Get last message hash
storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey)
let lastHash = storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
SnodeReceivedMessageInfo.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey)
let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, associatedWith: publicKey)?.hash ?? ""
// Construct signature
// let timestamp = UInt64(Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset)
// let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
@ -427,17 +446,28 @@ public final class SnodeAPI : NSObject {
public static func sendMessage(_ message: SnodeMessage) -> Promise<Set<RawResponsePromise>> {
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
let publicKey = Features.useTestnet ? message.recipient.removing05PrefixIfNeeded() : message.recipient
let publicKey = (Features.useTestnet ?
message.recipient.removing05PrefixIfNeeded() :
message.recipient
)
Threading.workQueue.async {
getTargetSnodes(for: publicKey).map2 { targetSnodes in
let parameters = message.toJSON()
return Set(targetSnodes.map { targetSnode in
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
}
})
}.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
getTargetSnodes(for: publicKey)
.map2 { targetSnodes in
let parameters = message.toJSON()
return targetSnodes
.map { targetSnode in
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
}
}
.asSet()
}
.done2 { seal.fulfill($0) }
.catch2 { seal.reject($0) }
}
return promise
}
@ -446,75 +476,123 @@ public final class SnodeAPI : NSObject {
AnyPromise.from(deleteMessage(publicKey: publicKey, serverHashes: serverHashes))
}
public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String:Bool]> {
let storage = SNSnodeKitConfiguration.shared.storage
guard let userX25519PublicKey = storage.getUserPublicKey(),
let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
let publicKey = Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey
public static func deleteMessage(publicKey: String, serverHashes: [String]) -> Promise<[String: Bool]> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Promise(error: Error.noKeyPair)
}
let publicKey = (Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey)
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: publicKey).then2 { swarm -> Promise<[String:Bool]> in
let snode = swarm.randomElement()!
let verificationData = (Legacy.Snode.Method.deleteMessage.rawValue + serverHashes.joined(separator: "")).data(using: String.Encoding.utf8)!
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed }
let parameters: JSON = [
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"messages": serverHashes,
"signature": signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters).map2{ rawResponse -> [String:Bool] in
guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
var result: [String:Bool] = [:]
for (snodePublicKey, rawJSON) in swarm {
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
let isFailed = json["failed"] as? Bool ?? false
if !isFailed {
guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON }
// The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
let verificationData = (userX25519PublicKey + serverHashes.joined(separator: "") + hashes.joined(separator: "")).data(using: String.Encoding.utf8)!
let isValid = sodium.sign.verify(message: Bytes(verificationData), publicKey: Bytes(Data(hex: snodePublicKey)), signature: Bytes(Data(base64Encoded: signature)!))
result[snodePublicKey] = isValid
} else {
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
} else {
SNLog("Couldn't delete data from: \(snodePublicKey).")
}
result[snodePublicKey] = false
getSwarm(for: publicKey)
.then2 { swarm -> Promise<[String: Bool]> in
guard
let snode = swarm.randomElement(),
let verificationData = (Endpoint.deleteMessage.rawValue + serverHashes.joined()).data(using: String.Encoding.utf8),
let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey)
else {
throw Error.signingFailed
}
let parameters: JSON = [
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"messages": serverHashes,
"signature": signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.deleteMessage, on: snode, associatedWith: publicKey, parameters: parameters)
.map2 { rawResponse -> [String: Bool] in
guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else {
throw HTTP.Error.invalidJSON
}
var result: [String: Bool] = [:]
for (snodePublicKey, rawJSON) in swarm {
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
let isFailed = (json["failed"] as? Bool ?? false)
if !isFailed {
guard
let hashes = json["deleted"] as? [String],
let signature = json["signature"] as? String
else {
throw HTTP.Error.invalidJSON
}
// The signature format is ( PUBKEY_HEX || RMSG[0] || ... || RMSG[N] || DMSG[0] || ... || DMSG[M] )
let verificationData = [
userX25519PublicKey,
serverHashes.joined(),
hashes.joined()
]
.joined()
.data(using: String.Encoding.utf8)!
let isValid = sodium.sign.verify(
message: Bytes(verificationData),
publicKey: Bytes(Data(hex: snodePublicKey)),
signature: Bytes(Data(base64Encoded: signature)!)
)
result[snodePublicKey] = isValid
}
else {
if let reason = json["reason"] as? String, let statusCode = json["code"] as? String {
SNLog("Couldn't delete data from: \(snodePublicKey) due to error: \(reason) (\(statusCode)).")
} else {
SNLog("Couldn't delete data from: \(snodePublicKey).")
}
result[snodePublicKey] = false
}
}
return result
}
return result
}
}
}
}
}
/// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
public static func clearAllData() -> Promise<[String:Bool]> {
let storage = SNSnodeKitConfiguration.shared.storage
guard let userX25519PublicKey = storage.getUserPublicKey(),
let userED25519KeyPair = storage.getUserED25519KeyPair() else { return Promise(error: Error.noKeyPair) }
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Promise(error: Error.noKeyPair)
}
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: userX25519PublicKey).then2 { swarm -> Promise<[String:Bool]> in
let snode = swarm.randomElement()!
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getNetworkTime(from: snode).then2 { timestamp -> Promise<[String:Bool]> in
let verificationData = (Legacy.Snode.Method.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)!
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed }
let verificationData = (Endpoint.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)!
guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else {
throw Error.signingFailed
}
let parameters: JSON = [
"pubkey" : userX25519PublicKey,
"pubkey_ed25519" : userED25519KeyPair.publicKey.toHexString(),
"timestamp" : timestamp,
"signature" : signature.toBase64()
"pubkey": userX25519PublicKey,
"pubkey_ed25519": userED25519KeyPair.publicKey.toHexString(),
"timestamp": timestamp,
"signature": signature.toBase64()
]
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String:Bool] in
guard let json = rawResponse as? JSON, let swarm = json["swarm"] as? JSON else { throw HTTP.Error.invalidJSON }
var result: [String:Bool] = [:]
invoke(.clearAllData, on: snode, parameters: parameters).map2 { rawResponse -> [String: Bool] in
guard
let json = rawResponse as? JSON,
let swarm = json["swarm"] as? JSON
else { throw HTTP.Error.invalidJSON }
var result: [String: Bool] = [:]
for (snodePublicKey, rawJSON) in swarm {
guard let json = rawJSON as? JSON else { throw HTTP.Error.invalidJSON }
let isFailed = json["failed"] as? Bool ?? false
if !isFailed {
guard let hashes = json["deleted"] as? [String], let signature = json["signature"] as? String else { throw HTTP.Error.invalidJSON }
@ -531,6 +609,7 @@ public final class SnodeAPI : NSObject {
result[snodePublicKey] = false
}
}
return result
}
}
@ -544,66 +623,73 @@ public final class SnodeAPI : NSObject {
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
private static func parseSnodes(from rawResponse: Any) -> Set<Legacy.Snode> {
private static func parseSnodes(from rawResponse: Any) -> Set<Snode> {
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
SNLog("Failed to parse snodes from: \(rawResponse).")
return []
}
return Set(rawSnodes.compactMap { rawSnode in
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString),
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
}
return Legacy.Snode(address: "https://\(address)", port: port, publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
}
public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Legacy.Snode, associatedWith publicKey: String) -> (messages: [JSON], lastRawMessage: JSON?) {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return ([], nil) }
return (
removeDuplicates(from: rawMessages, associatedWith: publicKey),
rawMessages.last
)
}
public static func updateLastMessageHashValueIfPossible(for snode: Legacy.Snode, associatedWith publicKey: String, from lastRawMessage: JSON?) {
if let lastMessage = lastRawMessage, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey,
to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
}
} else if (lastRawMessage != nil) {
SNLog("Failed to update last message hash value from: \(String(describing: lastRawMessage)).")
guard let snodeData: Data = try? JSONSerialization.data(withJSONObject: rawSnodes, options: []) else {
return []
}
// FIXME: Hopefully at some point this different Snode structure will be deprecated and can be removed
if
let swarmSnodes: [SwarmSnode] = try? JSONDecoder().decode([Failable<SwarmSnode>].self, from: snodeData).compactMap({ $0.value }),
!swarmSnodes.isEmpty
{
return swarmSnodes.map { $0.toSnode() }.asSet()
}
return ((try? JSONDecoder().decode([Failable<Snode>].self, from: snodeData)) ?? [])
.compactMap { $0.value }
.asSet()
}
public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [SnodeReceivedMessage] {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else {
return []
}
return removeDuplicates(from: rawMessages, associatedWith: publicKey)
.compactMap { rawMessage -> SnodeReceivedMessage? in
return SnodeReceivedMessage(
snode: snode,
publicKey: publicKey,
rawMessage: rawMessage
)
}
}
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
let oldReceivedMessages = SNSnodeKitConfiguration.shared.storage.getReceivedMessages(for: publicKey)
var newReceivedMessages = oldReceivedMessages
let result = rawMessages.filter { rawMessage in
guard let hash = rawMessage["hash"] as? String else {
SNLog("Missing hash value for message: \(rawMessage).")
return false
}
let isDuplicate = newReceivedMessages.contains(hash)
newReceivedMessages.insert(hash)
return !isDuplicate
var oldReceivedMessages: [SnodeReceivedMessageInfo] = []
GRDBStorage.shared.read { db in
oldReceivedMessages = oldReceivedMessages.appending(
contentsOf: try? SnodeReceivedMessageInfo
.filter(SnodeReceivedMessageInfo.Columns.key.like("%\(publicKey)"))
.fetchAll(db)
)
}
// Avoid the sync write transaction if possible
if oldReceivedMessages != newReceivedMessages {
SNSnodeKitConfiguration.shared.storage.writeSync { transaction in
SNSnodeKitConfiguration.shared.storage.setReceivedMessages(to: newReceivedMessages, for: publicKey, using: transaction)
let oldMessageHashes: Set<String> = oldReceivedMessages.map { $0.hash }.asSet()
return rawMessages
.compactMap { rawMessage -> JSON? in
guard let hash: String = rawMessage["hash"] as? String else {
SNLog("Missing hash value for message: \(rawMessage).")
return nil
}
guard !oldMessageHashes.contains(hash) else { return nil }
return rawMessage
}
}
return result
}
// MARK: Error Handling
/// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions.
@discardableResult
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Legacy.Snode, associatedWith publicKey: String? = nil) -> Error? {
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif

View File

@ -1,49 +0,0 @@
import SessionUtilitiesKit
extension Storage {
private static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
public func getOnionRequestPaths() -> [OnionRequestAPI.Path] {
let collection = Storage.onionRequestPathCollection
var result: [OnionRequestAPI.Path] = []
Storage.read { transaction in
if
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? Legacy.Snode,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? Legacy.Snode,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? Legacy.Snode {
result.append([ path0Snode0, path0Snode1, path0Snode2 ])
if
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? Legacy.Snode,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? Legacy.Snode,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? Legacy.Snode {
result.append([ path1Snode0, path1Snode1, path1Snode2 ])
}
}
}
return result
}
public func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any) {
let collection = Storage.onionRequestPathCollection
// FIXME: This approach assumes either 1 or 2 paths of length 3 each. We should do better than this.
clearOnionRequestPaths(using: transaction)
guard let transaction = transaction as? YapDatabaseReadWriteTransaction else { return }
guard paths.count >= 1 else { return }
let path0 = paths[0]
guard path0.count == 3 else { return }
transaction.setObject(path0[0], forKey: "0-0", inCollection: collection)
transaction.setObject(path0[1], forKey: "0-1", inCollection: collection)
transaction.setObject(path0[2], forKey: "0-2", inCollection: collection)
guard paths.count >= 2 else { return }
let path1 = paths[1]
guard path1.count == 3 else { return }
transaction.setObject(path1[0], forKey: "1-0", inCollection: collection)
transaction.setObject(path1[1], forKey: "1-1", inCollection: collection)
transaction.setObject(path1[2], forKey: "1-2", inCollection: collection)
}
func clearOnionRequestPaths(using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: Storage.onionRequestPathCollection)
}
}

View File

@ -1,140 +0,0 @@
import SessionUtilitiesKit
extension Storage {
// MARK: - Snode Pool
private static let snodePoolCollection = "LokiSnodePoolCollection"
private static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection"
public func getSnodePool() -> Set<Legacy.Snode> {
var result: Set<Legacy.Snode> = []
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in
guard let snode = object as? Legacy.Snode else { return }
result.insert(snode)
}
}
return result
}
public func setSnodePool(to snodePool: Set<Legacy.Snode>, using transaction: Any) {
clearSnodePool(in: transaction)
snodePool.forEach { snode in
(transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection)
}
}
public func clearSnodePool(in transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: Storage.snodePoolCollection)
}
public func getLastSnodePoolRefreshDate() -> Date? {
var result: Date?
Storage.read { transaction in
result = transaction.object(forKey: "lastSnodePoolRefreshDate", inCollection: Storage.lastSnodePoolRefreshDateCollection) as? Date
}
return result
}
public func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(date, forKey: "lastSnodePoolRefreshDate", inCollection: Storage.lastSnodePoolRefreshDateCollection)
}
// MARK: - Swarm
private static func getSwarmCollection(for publicKey: String) -> String {
return "LokiSwarmCollection-\(publicKey)"
}
public func getSwarm(for publicKey: String) -> Set<Legacy.Snode> {
var result: Set<Legacy.Snode> = []
let collection = Storage.getSwarmCollection(for: publicKey)
Storage.read { transaction in
transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in
guard let snode = object as? Legacy.Snode else { return }
result.insert(snode)
}
}
return result
}
public func setSwarm(to swarm: Set<Legacy.Snode>, for publicKey: String, using transaction: Any) {
clearSwarm(for: publicKey, in: transaction)
let tmp = getSnodePool()
let collection = Storage.getSwarmCollection(for: publicKey)
swarm.forEach { snode in
(transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: collection)
}
}
public func clearSwarm(for publicKey: String, in transaction: Any) {
let collection = Storage.getSwarmCollection(for: publicKey)
(transaction as! YapDatabaseReadWriteTransaction).removeAllObjects(inCollection: collection)
}
// MARK: - Last Message Hash
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
public func getLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String) -> JSON? {
let key = "\(snode.address):\(snode.port).\(publicKey)"
var result: JSON?
Storage.read { transaction in
result = transaction.object(forKey: key, inCollection: Storage.lastMessageHashCollection) as? JSON
}
if let result = result {
guard result["hash"] as? String != nil else { return nil }
guard result["expirationDate"] as? NSNumber != nil else { return nil }
}
return result
}
public func getLastMessageHash(for snode: Legacy.Snode, associatedWith publicKey: String) -> String? {
return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String
}
public func setLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return }
(transaction as! YapDatabaseReadWriteTransaction).setObject(lastMessageHashInfo, forKey: key, inCollection: Storage.lastMessageHashCollection)
}
public func pruneLastMessageHashInfoIfExpired(for snode: Legacy.Snode, associatedWith publicKey: String) {
guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey),
(lastMessageHashInfo["hash"] as? String) != nil, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return }
let now = NSDate.millisecondTimestamp()
if now >= expirationDate {
Storage.writeSync { transaction in
self.removeLastMessageHashInfo(for: snode, associatedWith: publicKey, using: transaction)
}
}
}
public func removeLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, using transaction: Any) {
let key = "\(snode.address):\(snode.port).\(publicKey)"
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: Storage.lastMessageHashCollection)
}
// MARK: - Received Messages
private static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
public func getReceivedMessages(for publicKey: String) -> Set<String> {
var result: Set<String>?
Storage.read { transaction in
result = transaction.object(forKey: publicKey, inCollection: Storage.receivedMessagesCollection) as? Set<String>
}
return result ?? []
}
public func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: Any) {
(transaction as! YapDatabaseReadWriteTransaction).setObject(receivedMessages, forKey: publicKey, inCollection: Storage.receivedMessagesCollection)
}
}

View File

@ -1,28 +0,0 @@
import SessionUtilitiesKit
import PromiseKit
import Sodium
public protocol SessionSnodeKitStorageProtocol {
@discardableResult
func write(with block: @escaping (Any) -> Void) -> Promise<Void>
@discardableResult
func write(with block: @escaping (Any) -> Void, completion: @escaping () -> Void) -> Promise<Void>
func writeSync(with block: @escaping (Any) -> Void)
func getUserPublicKey() -> String?
func getUserED25519KeyPair() -> Box.KeyPair?
func getOnionRequestPaths() -> [OnionRequestAPI.Path]
func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any)
func getSnodePool() -> Set<Legacy.Snode>
func setSnodePool(to snodePool: Set<Legacy.Snode>, using transaction: Any)
func getLastSnodePoolRefreshDate() -> Date?
func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any)
func getSwarm(for publicKey: String) -> Set<Legacy.Snode>
func setSwarm(to swarm: Set<Legacy.Snode>, for publicKey: String, using transaction: Any)
func getLastMessageHash(for snode: Legacy.Snode, associatedWith publicKey: String) -> String?
func setLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any)
func pruneLastMessageHashInfoIfExpired(for snode: Legacy.Snode, associatedWith publicKey: String)
func getReceivedMessages(for publicKey: String) -> Set<String>
func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: Any)
}

View File

@ -0,0 +1,17 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension OnionRequestAPI {
public enum Destination: CustomStringConvertible {
case snode(Snode)
case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?)
public var description: String {
switch self {
case .snode(let snode): return "Service node \(snode.ip):\(snode.port)"
case .server(let host, _, _, _, _): return host
}
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension SnodeAPI {
public enum Endpoint: String {
case getSwarm = "get_snodes_for_pubkey"
case getMessages = "retrieve"
case sendMessage = "store"
case deleteMessage = "delete"
case oxenDaemonRPCCall = "oxend_request"
case getInfo = "info"
case clearAllData = "delete_all"
}
}

View File

@ -0,0 +1,63 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
extension OnionRequestAPI {
public enum Error: LocalizedError {
case httpRequestFailedAtDestination(statusCode: UInt, json: JSON, destination: Destination)
case insufficientSnodes
case invalidURL
case missingSnodeVersion
case snodePublicKeySetMissing
case unsupportedSnodeVersion(String)
public var errorDescription: String? {
switch self {
case .httpRequestFailedAtDestination(let statusCode, _, let destination):
if statusCode == 429 { return "Rate limited." }
return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)."
case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path."
case .invalidURL: return "Invalid URL"
case .missingSnodeVersion: return "Missing Service Node version."
case .snodePublicKeySetMissing: return "Missing Service Node public key set."
case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)."
}
}
}
}
extension SnodeAPI {
public enum Error: LocalizedError {
case generic
case clockOutOfSync
case snodePoolUpdatingFailed
case inconsistentSnodePools
case noKeyPair
case signingFailed
case invalidIP
// ONS
case decryptionFailed
case hashingFailed
case validationFailed
public var errorDescription: String? {
switch self {
case .generic: return "An error occurred."
case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time."
case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool."
case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network."
case .noKeyPair: return "Missing user key pair."
case .signingFailed: return "Couldn't sign message."
case .invalidIP: return "Invalid IP."
// ONS
case .decryptionFailed: return "Couldn't decrypt ONS name."
case .hashingFailed: return "Couldn't compute ONS name hash."
case .validationFailed: return "ONS name validation failed."
}
}
}
}

View File

@ -13,6 +13,17 @@ public final class SNUtilitiesKitConfiguration : NSObject {
}
public enum SNUtilitiesKit { // Just to make the external API nice
public static func migrations() -> TargetMigrations {
return TargetMigrations(
identifier: .utilitiesKit,
migrations: [
[
_001_InitialSetupMigration.self,
_002_YDBToGRDBMigration.self
]
]
)
}
public static func configure(owsPrimaryStorage: OWSPrimaryStorageProtocol, maxFileSize: UInt) {
SNUtilitiesKitConfiguration.shared = SNUtilitiesKitConfiguration(owsPrimaryStorage: owsPrimaryStorage, maxFileSize: maxFileSize)

View File

@ -210,15 +210,15 @@ public final class GRDBStorage {
// MARK: - Functions
public func write<T>(updates: (Database) throws -> T) throws -> T {
return try dbPool.write(updates)
@discardableResult public func write<T>(updates: (Database) throws -> T?) -> T? {
return try? dbPool.write(updates)
}
public func writeAsync<T>(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Result<T, Error>) -> Void) {
dbPool.asyncWrite(updates, completion: completion)
}
public func read<T>(_ value: (Database) throws -> T) throws -> T {
return try dbPool.read(value)
@discardableResult public func read<T>(_ value: (Database) throws -> T?) -> T? {
return try? dbPool.read(value)
}
}

View File

@ -0,0 +1,25 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
public enum Legacy {
// MARK: - Collections and Keys
internal static let userAccountRegisteredNumberKey = "TSStorageRegisteredNumberKey"
internal static let userAccountCollection = "TSStorageUserAccountCollection"
internal static let identityKeyStoreSeedKey = "LKLokiSeed"
internal static let identityKeyStoreEd25519SecretKey = "LKED25519SecretKey"
internal static let identityKeyStoreEd25519PublicKey = "LKED25519PublicKey"
internal static let identityKeyStoreIdentityKey = "TSStorageManagerIdentityKeyStoreIdentityKey"
internal static let identityKeyStoreCollection = "TSStorageManagerIdentityKeyStoreCollection"
}
// MARK: - Legacy Extensions
internal extension YapDatabaseReadTransaction {
func keyPair(forKey key: String, in collection: String) -> ECKeyPair? {
return (self.object(forKey: key, inCollection: collection) as? ECKeyPair)
}
}

View File

@ -0,0 +1,26 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
enum _001_InitialSetupMigration: Migration {
static let identifier: String = "initialSetup"
static func migrate(_ db: Database) throws {
try db.create(table: Identity.self) { t in
t.column(.variant, .text)
.notNull()
.unique()
.primaryKey()
t.column(.data, .blob).notNull()
}
try db.create(table: Setting.self) { t in
t.column(.key, .text)
.notNull()
.unique()
.primaryKey()
t.column(.value, .blob).notNull()
}
}
}

View File

@ -0,0 +1,89 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Curve25519Kit
enum _002_YDBToGRDBMigration: Migration {
static let identifier: String = "YDBToGRDBMigration"
static func migrate(_ db: Database) throws {
// MARK: - Identity keys
// Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult'
var registeredNumber: String?
var seedHexString: String?
var userEd25519SecretKeyHexString: String?
var userEd25519PublicKeyHexString: String?
var userX25519KeyPair: ECKeyPair?
Storage.read { transaction in
registeredNumber = transaction.object(
forKey: Legacy.userAccountRegisteredNumberKey,
inCollection: Legacy.userAccountCollection
) as? String
// Note: The 'seed', 'ed25519SecretKey' and 'ed25519PublicKey' were
// all previously stored as hex strings, so we need to convert them
// to data before we store them in the new database
seedHexString = transaction.object(
forKey: Legacy.identityKeyStoreSeedKey,
inCollection: Legacy.identityKeyStoreCollection
) as? String
userEd25519SecretKeyHexString = transaction.object(
forKey: Legacy.identityKeyStoreEd25519SecretKey,
inCollection: Legacy.identityKeyStoreCollection
) as? String
userEd25519PublicKeyHexString = transaction.object(
forKey: Legacy.identityKeyStoreEd25519PublicKey,
inCollection: Legacy.identityKeyStoreCollection
) as? String
userX25519KeyPair = transaction.keyPair(
forKey: Legacy.identityKeyStoreIdentityKey,
in: Legacy.identityKeyStoreCollection
)
}
// No need to continue if the user isn't registered
if registeredNumber == nil { return }
// If the user is registered then it's all-or-nothing for these values
guard
let seedHexString: String = seedHexString,
let userEd25519SecretKeyHexString: String = userEd25519SecretKeyHexString,
let userEd25519PublicKeyHexString: String = userEd25519PublicKeyHexString,
let userX25519KeyPair: ECKeyPair = userX25519KeyPair
else {
throw GRDBStorageError.migrationFailed
}
// Insert the data into GRDB
try Identity(
variant: .seed,
data: Data(hex: seedHexString)
).insert(db)
try Identity(
variant: .ed25519SecretKey,
data: Data(hex: userEd25519SecretKeyHexString)
).insert(db)
try Identity(
variant: .ed25519PublicKey,
data: Data(hex: userEd25519PublicKeyHexString)
).insert(db)
try Identity(
variant: .x25519PrivateKey,
data: userX25519KeyPair.privateKey
).insert(db)
try Identity(
variant: .x25519PublicKey,
data: userX25519KeyPair.publicKey
).insert(db)
}
}

View File

@ -0,0 +1,129 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import Curve25519Kit
import CryptoSwift
public struct Identity: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "identity" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case variant
case data
}
public enum Variant: String, Codable, DatabaseValueConvertible {
case seed
case ed25519SecretKey
case ed25519PublicKey
case x25519PrivateKey
case x25519PublicKey
}
public var id: Variant { variant }
let variant: Variant
let data: Data
}
// MARK: - Convenience
extension ECKeyPair {
func toData() -> Data {
var targetValue: ECKeyPair = self
return Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
}
}
// MARK: - User Identity
public extension Identity {
static func generate(from seed: Data) throws -> (ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
assert(seed.count == 16)
let padding = Data(repeating: 0, count: 16)
guard
let ed25519KeyPair = Sodium().sign.keyPair(seed: (seed + padding).bytes),
let x25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: ed25519KeyPair.publicKey),
let x25519SecretKey = Sodium().sign.toX25519(ed25519SecretKey: ed25519KeyPair.secretKey)
else {
throw GeneralError.keyGenerationFailed
}
let x25519KeyPair = try ECKeyPair(publicKeyData: Data(x25519PublicKey), privateKeyData: Data(x25519SecretKey))
return (ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
}
static func store(seed: Data, ed25519KeyPair: Sign.KeyPair, x25519KeyPair: ECKeyPair) {
GRDBStorage.shared.write { db in
try Identity(variant: .seed, data: seed).save(db)
try Identity(variant: .ed25519SecretKey, data: Data(ed25519KeyPair.secretKey)).save(db)
try Identity(variant: .ed25519PublicKey, data: Data(ed25519KeyPair.publicKey)).save(db)
try Identity(variant: .x25519PrivateKey, data: x25519KeyPair.privateKey).save(db)
try Identity(variant: .x25519PublicKey, data: x25519KeyPair.publicKey).save(db)
}
}
static func fetchUserKeyPair() -> ECKeyPair? {
return GRDBStorage.shared.read { db -> ECKeyPair? in
guard
let publicKey: Identity = try? Identity.fetchOne(db, id: .x25519PublicKey),
let privateKey: Identity = try? Identity.fetchOne(db, id: .x25519PrivateKey)
else {
return nil
}
return try? ECKeyPair(
publicKeyData: publicKey.data,
privateKeyData: privateKey.data
)
}
}
static func fetchUserEd25519KeyPair() -> Box.KeyPair? {
return GRDBStorage.shared.read { db -> Box.KeyPair? in
guard
let publicKey: Identity = try? Identity.fetchOne(db, id: .ed25519PublicKey),
let secretKey: Identity = try? Identity.fetchOne(db, id: .ed25519SecretKey)
else {
return nil
}
return Box.KeyPair(
publicKey: publicKey.data.bytes,
secretKey: secretKey.data.bytes
)
}
}
static func fetchHexEncodedSeed() -> String? {
return GRDBStorage.shared.read { db in
guard let value: Identity = try? Identity.fetchOne(db, id: .seed) else {
return nil
}
return value.data.toHexString()
}
}
// TODO: Should this actually clear all identity values???
static func clearUserKeyPair() {
GRDBStorage.shared.write { db in
try Identity.deleteOne(db, id: .x25519PublicKey)
try Identity.deleteOne(db, id: .x25519PrivateKey)
}
}
}
@objc(SUKIdentity)
public class objc_Identity: NSObject {
@objc(clearUserKeyPair)
public static func objc_clearUserKeyPair() {
Identity.clearUserKeyPair()
}
}

View File

@ -0,0 +1,149 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
// MARK: - Setting
public struct Setting: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "settings" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case key
case value
}
public var id: String { key }
let key: String
let value: Data
}
extension Setting {
fileprivate init?<T>(key: String, value: T?) {
guard let value: T = value else { return nil }
var targetValue: T = value
self.key = key
self.value = Data(bytes: &targetValue, count: MemoryLayout.size(ofValue: targetValue))
}
fileprivate func value<T>(as type: T.Type) -> T {
return value.withUnsafeBytes { $0.load(as: T.self) }
}
}
// MARK: - Keys
public extension Setting {
struct BoolKey: RawRepresentable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) { self.rawValue = rawValue }
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.init(value) }
public init(unicodeScalarLiteral value: String) { self.init(value) }
public init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}
struct DateKey: RawRepresentable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) { self.rawValue = rawValue }
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.init(value) }
public init(unicodeScalarLiteral value: String) { self.init(value) }
public init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}
struct DoubleKey: RawRepresentable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) { self.rawValue = rawValue }
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.init(value) }
public init(unicodeScalarLiteral value: String) { self.init(value) }
public init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}
struct IntKey: RawRepresentable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) { self.rawValue = rawValue }
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.init(value) }
public init(unicodeScalarLiteral value: String) { self.init(value) }
public init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}
struct StringKey: RawRepresentable, ExpressibleByStringLiteral {
public let rawValue: String
public init(_ rawValue: String) { self.rawValue = rawValue }
public init?(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.init(value) }
public init(unicodeScalarLiteral value: String) { self.init(value) }
public init(extendedGraphemeClusterLiteral value: String) { self.init(value) }
}
}
// MARK: - Database Access
public extension GRDBStorage {
subscript(key: Setting.BoolKey) -> Bool? { return read { db in db[key] } }
subscript(key: Setting.DoubleKey) -> Double? { return read { db in db[key] } }
subscript(key: Setting.IntKey) -> Int? { return read { db in db[key] } }
subscript(key: Setting.StringKey) -> String? { return read { db in db[key] } }
subscript(key: Setting.DateKey) -> Date? { return read { db in db[key] } }
}
public extension Database {
private subscript(key: String) -> Setting? {
get { try? Setting.filter(id: key).fetchOne(self) }
set {
guard let newValue: Setting = newValue else {
_ = try? Setting.filter(id: key).deleteAll(self)
return
}
try? newValue.save(self)
}
}
subscript(key: Setting.BoolKey) -> Bool? {
get { self[key.rawValue]?.value(as: Bool.self) }
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
}
subscript(key: Setting.DoubleKey) -> Double? {
get { self[key.rawValue]?.value(as: Double.self) }
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
}
subscript(key: Setting.IntKey) -> Int? {
get { self[key.rawValue]?.value(as: Int.self) }
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
}
subscript(key: Setting.StringKey) -> String? {
get { self[key.rawValue]?.value(as: String.self) }
set { self[key.rawValue] = Setting(key: key.rawValue, value: newValue) }
}
/// Value will be stored as a timestamp in seconds since 1970
subscript(key: Setting.DateKey) -> Date? {
get {
let timestamp: TimeInterval? = self[key.rawValue]?.value(as: TimeInterval.self)
return timestamp.map { Date(timeIntervalSince1970: $0) }
}
set {
self[key.rawValue] = Setting(
key: key.rawValue,
value: newValue.map { $0.timeIntervalSince1970 }
)
}
}
}

View File

@ -1,3 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -16,6 +16,7 @@ public struct TargetMigrations: Comparable {
public enum Identifier: String, CaseIterable, Comparable {
// WARNING: The string version of these cases are used as migration identifiers so
// changing them will result in the migrations running again
case utilitiesKit
case snodeKit
case messagingKit

View File

@ -20,7 +20,14 @@ public class TypedTableDefinition<T> where T: TableRecord, T: ColumnExpressible
definition.primaryKey(columns.map { $0.name }, onConflict: onConflict)
}
public func foreignKey<Other>(_ columns: [T.Columns], references table: Other.Type, columns destinationColumns: [Other.Columns]? = nil, onDelete: Database.ForeignKeyAction? = nil, onUpdate: Database.ForeignKeyAction? = nil, deferred: Bool = false) where Other: TableRecord, Other: ColumnExpressible {
public func foreignKey<Other>(
_ columns: [T.Columns],
references table: Other.Type,
columns destinationColumns: [Other.Columns]? = nil,
onDelete: Database.ForeignKeyAction? = nil,
onUpdate: Database.ForeignKeyAction? = nil,
deferred: Bool = false
) where Other: TableRecord, Other: ColumnExpressible {
return definition.foreignKey(
columns.map { $0.name },
references: table.databaseTableName,

View File

@ -6,6 +6,25 @@ public extension Array where Element : CustomStringConvertible {
}
}
public extension Array {
func appending(_ other: Element?) -> [Element] {
guard let other: Element = other else { return self }
var updatedArray: [Element] = self
updatedArray.append(other)
return updatedArray
}
func appending(contentsOf other: [Element]?) -> [Element] {
guard let other: [Element] = other else { return self }
var updatedArray: [Element] = self
updatedArray.append(contentsOf: other)
return updatedArray
}
}
public extension Array where Element: Hashable {
func asSet() -> Set<Element> {
return Set(self)

View File

@ -10,4 +10,14 @@ public extension Dictionary {
return keyDescription + " : " + truncatedValueDescription
}.joined(separator: ", ") + " ]"
}
func asArray() -> [(key: Key, value: Value)] {
return Array(self)
}
}
public extension Dictionary.Values {
func asArray() -> [Value] {
return Array(self)
}
}

View File

@ -1,3 +1,35 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Curve25519Kit
public enum General {
public enum Cache {
public static var cachedEncodedPublicKey: Atomic<String?> = Atomic(nil)
}
}
public enum GeneralError: Error {
case keyGenerationFailed
}
@objc(SNGeneralUtilities)
public class GeneralUtilities: NSObject {
@objc public static func getUserPublicKey() -> String {
return getUserHexEncodedPublicKey()
}
}
public func getUserHexEncodedPublicKey() -> String {
if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey }
if let keyPair: ECKeyPair = Identity.fetchUserKeyPair() { // Can be nil under some circumstances
General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey }
return keyPair.hexEncodedPublicKey
}
return ""
}
/// Does nothing, but is never inlined and thus evaluating its argument will never be optimized away.
///

View File

@ -0,0 +1,26 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
/// The `Failable<T>` type allows for coding an array of values without failing the entire array if a single
/// value fails to encode/decode correctly
public struct Failable<T: Codable>: Codable {
public let value: T?
public init(from decoder: Decoder) throws {
guard let container = try? decoder.singleValueContainer() else {
self.value = nil
return
}
self.value = try? container.decode(T.self)
}
public func encode(to encoder: Encoder) throws {
guard let value: T = value else { return }
var container: SingleValueEncodingContainer = encoder.singleValueContainer()
try container.encode(value)
}
}

View File

@ -1,3 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Clibsodium
import Sodium

View File

@ -24,6 +24,7 @@ public final class Configuration : NSObject {
//DispatchQueue.main.once
let storage: GRDBStorage? = try? GRDBStorage(
migrations: [
SNUtilitiesKit.migrations(),
SNSnodeKit.migrations(),
SNMessagingKit.migrations()
]
@ -31,6 +32,6 @@ public final class Configuration : NSObject {
}
SNMessagingKit.configure(storage: Storage.shared)
SNSnodeKit.configure(storage: Storage.shared)
SNSnodeKit.configure()
}
}

View File

@ -1,5 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
extension Storage : SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {
import Foundation
extension Storage : SessionMessagingKitStorageProtocol {
public func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any) {
let transaction = transaction as! YapDatabaseReadWriteTransaction

View File

@ -4,7 +4,7 @@
#ifndef Signal_TSStorageHeaders_h
#define Signal_TSStorageHeaders_h
#import <SessionMessagingKit/OWSIdentityManager.h>
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
#import <SessionMessagingKit/OWSPrimaryStorage.h>

View File

@ -1,6 +1,5 @@
#import "OWSPrimaryStorage+Loki.h"
#import "OWSPrimaryStorage+keyFromIntLong.h"
#import "OWSIdentityManager.h"
#import "NSDate+OWS.h"
#import "TSAccountManager.h"
#import "YapDatabaseConnection+OWS.h"

View File

@ -9,7 +9,6 @@
#import <SignalUtilitiesKit/OWSProfileManager.h>
#import <SessionMessagingKit/OWSBackgroundTask.h>
#import <SessionMessagingKit/OWSDisappearingMessagesJob.h>
#import <SessionMessagingKit/OWSIdentityManager.h>
#import <SessionMessagingKit/OWSOutgoingReceiptManager.h>
#import <SessionMessagingKit/OWSReadReceiptManager.h>
#import <SessionMessagingKit/OWSSounds.h>
@ -51,7 +50,6 @@ NS_ASSUME_NONNULL_BEGIN
OWSPreferences *preferences = [OWSPreferences new];
OWSProfileManager *profileManager = [[OWSProfileManager alloc] initWithPrimaryStorage:primaryStorage];
OWSIdentityManager *identityManager = [[OWSIdentityManager alloc] initWithPrimaryStorage:primaryStorage];
TSAccountManager *tsAccountManager = [[TSAccountManager alloc] initWithPrimaryStorage:primaryStorage];
OWSDisappearingMessagesJob *disappearingMessagesJob =
[[OWSDisappearingMessagesJob alloc] initWithPrimaryStorage:primaryStorage];
@ -75,7 +73,6 @@ NS_ASSUME_NONNULL_BEGIN
[SSKEnvironment setShared:[[SSKEnvironment alloc] initWithProfileManager:profileManager
primaryStorage:primaryStorage
identityManager:identityManager
tsAccountManager:tsAccountManager
disappearingMessagesJob:disappearingMessagesJob
readReceiptManager:readReceiptManager