Started work on GRDB logic and migrations

Setup a migration pattern
Setup the database configuration and security
Started defining the database schema
Started working on the migrations for SessionSnodeKit
This commit is contained in:
Morgan Pretty 2022-03-31 11:47:09 +11:00
parent 1a6c34e3b8
commit 529e416dd1
33 changed files with 1126 additions and 159 deletions

View File

@ -765,6 +765,26 @@
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 */; };
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 */; };
FD17D7A127F40D2500122BE0 /* GRDBStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */; };
FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */; };
FD17D7A427F40F8100122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */; };
FD17D7A727F41AF000122BE0 /* SSKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */; };
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A927F41BF500122BE0 /* SnodeSet.swift */; };
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 */; };
FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */; };
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */; };
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 */; };
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; };
@ -1792,6 +1812,26 @@
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; };
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>"; };
FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKLegacyModels.swift; sourceTree = "<group>"; };
FD17D7A927F41BF500122BE0 /* SnodeSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeSet.swift; sourceTree = "<group>"; };
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>"; };
FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressible.swift; sourceTree = "<group>"; };
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableDefinition.swift; sourceTree = "<group>"; };
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>"; };
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = "<group>"; };
@ -2243,6 +2283,9 @@
B8A582AB258C64E800AFD84C /* Database */ = {
isa = PBXGroup;
children = (
FD17D7B427F51E6700122BE0 /* Types */,
FD17D7BB27F51F5C00122BE0 /* Utilities */,
FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */,
C33FDBAB255A581500E217F9 /* OWSFileSystem.h */,
C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */,
C3D9E41E25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift */,
@ -2338,6 +2381,7 @@
C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */,
C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */,
C33FDB14255A580800E217F9 /* OWSMath.h */,
FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */,
C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */,
C33FDB3F255A580C00E217F9 /* String+SSK.swift */,
C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */,
@ -2654,6 +2698,8 @@
C32C5BCB256DC818003C73A2 /* Database */ = {
isa = PBXGroup;
children = (
FD17D79A27F40ADA00122BE0 /* LegacyDatabase */,
FD17D79427F3E03300122BE0 /* Migrations */,
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */,
C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */,
C33FDB07255A580700E217F9 /* OWSBackupFragment.m */,
@ -3220,11 +3266,11 @@
isa = PBXGroup;
children = (
C3C2A5B0255385C700C340D1 /* Meta */,
FD17D79D27F40CAA00122BE0 /* Database */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */,
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */,
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */,
C3C2A5B7255385EC00C340D1 /* Snode.swift */,
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */,
C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */,
C3C2A5B8255385EC00C340D1 /* Storage.swift */,
@ -3579,6 +3625,92 @@
path = Session;
sourceTree = "<group>";
};
FD17D79427F3E03300122BE0 /* Migrations */ = {
isa = PBXGroup;
children = (
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */,
FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */,
);
path = Migrations;
sourceTree = "<group>";
};
FD17D79A27F40ADA00122BE0 /* LegacyDatabase */ = {
isa = PBXGroup;
children = (
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */,
);
path = LegacyDatabase;
sourceTree = "<group>";
};
FD17D79D27F40CAA00122BE0 /* Database */ = {
isa = PBXGroup;
children = (
FD17D7A527F41ADE00122BE0 /* LegacyDatabase */,
FD17D79E27F40CC000122BE0 /* Migrations */,
FD17D7A827F41BE300122BE0 /* Models */,
FD17D7B127F51E2B00122BE0 /* Types */,
);
path = Database;
sourceTree = "<group>";
};
FD17D79E27F40CC000122BE0 /* Migrations */ = {
isa = PBXGroup;
children = (
FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */,
FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */,
);
path = Migrations;
sourceTree = "<group>";
};
FD17D7A527F41ADE00122BE0 /* LegacyDatabase */ = {
isa = PBXGroup;
children = (
FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */,
);
path = LegacyDatabase;
sourceTree = "<group>";
};
FD17D7A827F41BE300122BE0 /* Models */ = {
isa = PBXGroup;
children = (
C3C2A5B7255385EC00C340D1 /* Snode.swift */,
FD17D7A927F41BF500122BE0 /* SnodeSet.swift */,
FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */,
);
path = Models;
sourceTree = "<group>";
};
FD17D7B127F51E2B00122BE0 /* Types */ = {
isa = PBXGroup;
children = (
FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD17D7B427F51E6700122BE0 /* Types */ = {
isa = PBXGroup;
children = (
FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */,
FD17D7B727F51ECA00122BE0 /* Migration.swift */,
FD17D7B527F51E7300122BE0 /* SettingType.swift */,
FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */,
FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD17D7BB27F51F5C00122BE0 /* Utilities */ = {
isa = PBXGroup;
children = (
FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */,
FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */,
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
FD659ABE27A7648200F12C02 /* Message Requests */ = {
isa = PBXGroup;
children = (
@ -4576,16 +4708,22 @@
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
C3C2A5BF255385EE00C340D1 /* SnodeMessage.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 */,
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 */,
FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */,
FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */,
C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */,
C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */,
);
@ -4597,6 +4735,7 @@
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 */,
@ -4604,6 +4743,7 @@
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */,
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
@ -4617,6 +4757,7 @@
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */,
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */,
FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */,
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
@ -4624,21 +4765,28 @@
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */,
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */,
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
FD705A92278D051200F16121 /* ReusableView.swift in Sources */,
FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */,
FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */,
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */,
FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */,
B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */,
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */,
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */,
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FD17D7A127F40D2500122BE0 /* GRDBStorage.swift in Sources */,
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */,
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */,
FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */,
B88FA7FB26114EA70049422F /* Hex.swift in Sources */,
C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */,
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
@ -4646,6 +4794,7 @@
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
B8856D23256F116B001CE70E /* Weak.swift in Sources */,
C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */,
FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */,
B87EF18126377A1D00124B3C /* Features.swift in Sources */,
C300A60D2554B31900555489 /* Logging.swift in Sources */,
B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */,
@ -4705,6 +4854,7 @@
C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */,
C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */,
B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */,
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */,
C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */,
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */,
C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */,
@ -4717,6 +4867,7 @@
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */,
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */,
C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */,
C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */,
C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */,
@ -4786,6 +4937,7 @@
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */,
C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */,
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */,
C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */,
C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */,
C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */,

View File

@ -156,7 +156,7 @@ final class PathVC : BaseVC {
return stackView
}
private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
private func getPathRow(snode: Legacy.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,3 +1,5 @@
import Foundation
import SessionUtilitiesKit
@objc
public final class SNMessagingKitConfiguration : NSObject {
@ -11,7 +13,20 @@ public final class SNMessagingKitConfiguration : NSObject {
}
public enum SNMessagingKit { // Just to make the external API nice
public static func migrations() -> TargetMigrations {
return TargetMigrations(
identifier: .messagingKit,
migrations: [
[
_001_InitialSetupMigration.self
],
[
_002_YDBToGRDBMigration.self
]
]
)
}
public static func configure(storage: SessionMessagingKitStorageProtocol) {
SNMessagingKitConfiguration.shared = SNMessagingKitConfiguration(storage: storage)
}

View File

@ -0,0 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
enum Legacy {
}

View File

@ -0,0 +1,49 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
// TODO: Remove/Move these
struct Place: Codable, FetchableRecord, PersistableRecord, ColumnExpressible {
static var databaseTableName: String { "place" }
public enum Columns: String, CodingKey, ColumnExpression {
case id
case name
}
let id: String
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

@ -0,0 +1,14 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
enum _002_YDBToGRDBMigration: Migration {
static let identifier: String = "YDBToGRDBMigration"
static func migrate(_ db: Database) throws {
}
}

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<Snode>()
private var usedSnodes = Set<SessionSnodeKit.Legacy.Snode>()
private var pollCount = 0
// MARK: Settings
@ -89,7 +89,7 @@ public final class Poller : NSObject {
}
}
private func poll(_ snode: Snode, seal longTermSeal: Resolver<Void>) -> Promise<Void> {
private func poll(_ snode: SessionSnodeKit.Legacy.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

View File

@ -1,3 +1,5 @@
import Foundation
import SessionUtilitiesKit
public struct SNSnodeKitConfiguration {
public let storage: SessionSnodeKitStorageProtocol
@ -6,6 +8,19 @@ public struct SNSnodeKitConfiguration {
}
public enum SNSnodeKit { // Just to make the external API nice
public static func migrations() -> TargetMigrations {
return TargetMigrations(
identifier: .snodeKit,
migrations: [
[
_001_InitialSetupMigration.self
],
[
_002_YDBToGRDBMigration.self
]
]
)
}
public static func configure(storage: SessionSnodeKitStorageProtocol) {
SNSnodeKitConfiguration.shared = SNSnodeKitConfiguration(storage: storage)

View File

@ -0,0 +1,80 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public enum Legacy {
// MARK: - Collections and Keys
internal static let swarmCollectionPrefix = "LokiSwarmCollection-"
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
public typealias LegacyOnionRequestAPIPath = [Snode]
@objc(Snode)
public final class Snode: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public let address: String
public let port: UInt16
public let publicKeySet: KeySet
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])
}
// 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
public let x25519Key: String
}
// MARK: Initialization
internal init(address: String, port: UInt16, publicKeySet: KeySet) {
self.address = address
self.port = port
self.publicKeySet = publicKeySet
}
// MARK: Coding
public init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
guard let idKey = coder.decodeObject(forKey: "idKey") as? String,
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String else { return nil }
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
coder.encode(publicKeySet.ed25519Key, forKey: "idKey")
coder.encode(publicKeySet.x25519Key, forKey: "encryptionKey")
}
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? Snode else { return false }
return address == other.address && port == other.port
}
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return address.hashValue ^ port.hashValue
}
}
}

View File

@ -0,0 +1,47 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
enum _001_InitialSetupMigration: Migration {
static let identifier: String = "initialSetup"
static func migrate(_ db: Database) throws {
try db.create(table: Snode.self) { t in
t.column(.address, .text).notNull()
t.column(.port, .integer).notNull()
t.column(.ed25519PublicKey, .text).notNull()
t.column(.x25519PublicKey, .text).notNull()
t.primaryKey([.address, .port])
}
try db.create(table: SnodeSet.self) { t in
t.column(.key, .text).notNull()
t.column(.nodeIndex, .integer).notNull()
t.column(.address, .text).notNull()
t.column(.port, .integer).notNull()
t.foreignKey(
[.address, .port],
references: Snode.self,
columns: [.address, .port],
onDelete: .cascade
)
t.primaryKey([.key, .nodeIndex])
}
try db.create(table: SnodeReceivedMessageInfo.self) { t in
t.column(.key, .text)
.notNull()
.indexed()
t.column(.hash, .text).notNull()
t.column(.expirationDateMs, .integer)
.notNull()
.indexed()
t.primaryKey([.key, .hash])
}
}
}

View File

@ -0,0 +1,138 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
enum _002_YDBToGRDBMigration: Migration {
static let identifier: String = "YDBToGRDBMigration"
// TODO: Autorelease pool???
static func migrate(_ db: Database) throws {
// MARK: - OnionRequestPath, Snode Pool & Swarm
// Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult'
var snodeResult: Set<Legacy.Snode> = []
var snodeSetResult: [String: Set<Legacy.Snode>] = [:]
Storage.read { transaction in
// Process the OnionRequestPaths
if
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode
{
snodeResult.insert(path0Snode0)
snodeResult.insert(path0Snode1)
snodeResult.insert(path0Snode2)
snodeSetResult["\(SnodeSet.onionRequestPathPrefix)0"] = [ path0Snode0, path0Snode1, path0Snode2 ]
if
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode
{
snodeResult.insert(path1Snode0)
snodeResult.insert(path1Snode1)
snodeResult.insert(path1Snode2)
snodeSetResult["\(SnodeSet.onionRequestPathPrefix)1"] = [ path1Snode0, path1Snode1, path1Snode2 ]
}
}
// Process the SnodePool
transaction.enumerateKeysAndObjects(inCollection: Legacy.snodePoolCollection) { _, object, _ in
guard let snode = object as? Legacy.Snode else { return }
snodeResult.insert(snode)
}
// Process the Swarms
var swarmCollections: Set<String> = []
transaction.enumerateCollections { collectionName, _ in
if collectionName.starts(with: Legacy.swarmCollectionPrefix) {
swarmCollections.insert(collectionName.substring(from: Legacy.swarmCollectionPrefix.count))
}
}
for swarmCollection in swarmCollections {
let collection: String = "\(Legacy.swarmCollectionPrefix)\(swarmCollection)"
transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in
guard let snode = object as? Legacy.Snode else { return }
snodeResult.insert(snode)
snodeSetResult[swarmCollection] = (snodeSetResult[swarmCollection] ?? Set()).inserting(snode)
}
}
}
try snodeResult.forEach { legacySnode in
try Snode(
address: legacySnode.address,
port: legacySnode.port,
ed25519PublicKey: legacySnode.publicKeySet.ed25519Key,
x25519PublicKey: legacySnode.publicKeySet.x25519Key
).insert(db)
}
try snodeSetResult.forEach { key, legacySnodeSet in
try legacySnodeSet.enumerated().forEach { nodeIndex, legacySnode in
// Note: In this case the 'nodeIndex' is irrelivant
try SnodeSet(
key: key,
nodeIndex: UInt(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)] = [:]
var receivedMessageResults: [String: Set<String>] = [:]
Storage.read { transaction in
// Extract the received message hashes
transaction.enumerateKeysAndObjects(inCollection: Legacy.receivedMessagesCollection) { key, object, _ in
guard let hashSet = object as? Set<String> else { return }
receivedMessageResults[key] = hashSet
}
// Retrieve the last message info
transaction.enumerateKeysAndObjects(inCollection: Legacy.lastMessageHashCollection) { key, object, _ in
guard let lastMessageJson = object as? JSON else { return }
guard let lastMessageHash: String = lastMessageJson["hash"] as? String else { return }
// Note: We remove the value from 'receivedMessageResults' as we don't want to default it's
// expiration value to 0
lastMessageResults[key] = (lastMessageHash, lastMessageJson)
receivedMessageResults[key] = receivedMessageResults[key]?.removing(lastMessageHash)
}
}
try receivedMessageResults.forEach { key, hashes in
try hashes.forEach { hash in
try SnodeReceivedMessageInfo(
key: key,
hash: hash,
expirationDateMs: 0
).insert(db)
}
}
try lastMessageResults.forEach { key, data in
try SnodeReceivedMessageInfo(
key: key,
hash: data.hash,
expirationDateMs: ((data.json["expirationDate"] as? Int64) ?? 0)
).insert(db)
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable {
public static var databaseTableName: String { "snode" }
public enum Columns: String, CodingKey, ColumnExpression {
case address
case port
case ed25519PublicKey
case x25519PublicKey
}
let address: String
let port: UInt16
let ed25519PublicKey: String
let x25519PublicKey: String
}

View File

@ -0,0 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
struct SnodeReceivedMessageInfo: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
static var databaseTableName: String { "snodeReceivedMessageInfo" }
public enum Columns: String, CodingKey, ColumnExpression {
case key
case hash
case expirationDateMs
}
let key: String
let hash: String
let expirationDateMs: Int64
}

View File

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
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 {
case key
case nodeIndex
case address
case port
}
let key: String
let nodeIndex: UInt
let address: String
let port: UInt16
var nodes: QueryInterfaceRequest<Snode> {
request(for: SnodeSet.nodes)
}
}

View File

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

View File

@ -8,9 +8,9 @@ public enum OnionRequestAPI {
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var pathFailureCount: [Path:UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
private static var snodeFailureCount: [Snode:UInt] = [:]
private static var snodeFailureCount: [Legacy.Snode:UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
public static var guardSnodes: Set<Snode> = []
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
// MARK: Settings
@ -29,7 +29,7 @@ public enum OnionRequestAPI {
// MARK: Destination
public enum Destination : CustomStringConvertible {
case snode(Snode)
case snode(Legacy.Snode)
case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?)
public var description: String {
@ -67,14 +67,14 @@ public enum OnionRequestAPI {
}
// MARK: Path
public typealias Path = [Snode]
public typealias Path = [Legacy.Snode]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data)
private typealias OnionBuildingResult = (guardSnode: Legacy.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: Snode) -> Promise<Void> {
private static func testSnode(_ snode: Legacy.Snode) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
@ -96,17 +96,17 @@ public enum OnionRequestAPI {
/// 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: [Snode]) -> Promise<Set<Snode>> {
private static func getGuardSnodes(reusing reusableGuardSnodes: [Legacy.Snode]) -> Promise<Set<Legacy.Snode>> {
if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
return Promise<Set<Legacy.Snode>> { $0.fulfill(guardSnodes) }
} else {
SNLog("Populating guard snode cache.")
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: Error.insufficientSnodes) }
func getGuardSnode() -> Promise<Snode> {
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<Snode> { $0.reject(Error.insufficientSnodes) } }
guard let candidate = unusedSnodes.randomElement() else { return Promise<Legacy.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
@ -167,7 +167,7 @@ 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: Snode?) -> Promise<Path> {
private static func getPath(excluding snode: Legacy.Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
var paths = OnionRequestAPI.paths
if paths.isEmpty {
@ -216,14 +216,14 @@ public enum OnionRequestAPI {
}
}
private static func dropGuardSnode(_ snode: Snode) {
private static func dropGuardSnode(_ snode: Legacy.Snode) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
guardSnodes = guardSnodes.filter { $0 != snode }
}
private static func drop(_ snode: Snode) throws {
private static func drop(_ snode: Legacy.Snode) throws {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -272,10 +272,10 @@ public enum OnionRequestAPI {
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var guardSnode: Legacy.Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult!
var snodeToExclude: Snode?
var snodeToExclude: Legacy.Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in
guardSnode = path.first!
@ -305,7 +305,7 @@ public enum OnionRequestAPI {
// MARK: Public API
/// Sends an onion request to `snode`. Builds new paths as needed.
public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<JSON> {
public static func sendOnionRequest(to snode: Legacy.Snode, invoking method: Legacy.Snode.Method, 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 }
@ -365,7 +365,7 @@ public enum OnionRequestAPI {
public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Snode?
var guardSnode: Legacy.Snode?
Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
guardSnode = intermediate.guardSnode

View File

@ -1,65 +0,0 @@
import Foundation
public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
public let address: String
public let port: UInt16
public let publicKeySet: KeySet
public var ip: String {
address.removingPrefix("https://")
}
// 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
public let x25519Key: String
}
// MARK: Initialization
internal init(address: String, port: UInt16, publicKeySet: KeySet) {
self.address = address
self.port = port
self.publicKeySet = publicKeySet
}
// MARK: Coding
public init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
guard let idKey = coder.decodeObject(forKey: "idKey") as? String,
let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String else { return nil }
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
super.init()
}
public func encode(with coder: NSCoder) {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
coder.encode(publicKeySet.ed25519Key, forKey: "idKey")
coder.encode(publicKeySet.x25519Key, forKey: "encryptionKey")
}
// MARK: Equality
override public func isEqual(_ other: Any?) -> Bool {
guard let other = other as? Snode else { return false }
return address == other.address && port == other.port
}
// MARK: Hashing
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
return address.hashValue ^ port.hashValue
}
// MARK: Description
override public var description: String { return "\(address):\(port)" }
}

View File

@ -8,12 +8,12 @@ public final class SnodeAPI : NSObject {
private static var hasLoadedSnodePool = false
private static var loadedSwarms: Set<String> = []
private static var getSnodePoolPromise: Promise<Set<Snode>>?
private static var getSnodePoolPromise: Promise<Set<Legacy.Snode>>?
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
internal static var snodeFailureCount: [Snode:UInt] = [:]
internal static var snodeFailureCount: [Legacy.Snode:UInt] = [:]
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
internal static var snodePool: Set<Snode> = []
internal static var snodePool: Set<Legacy.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 +21,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<Snode>] = [:]
public static var swarmCache: [String:Set<Legacy.Snode>] = [:]
// MARK: Settings
private static let maxRetryCount: UInt = 8
@ -72,7 +72,7 @@ public final class SnodeAPI : NSObject {
hasLoadedSnodePool = true
}
private static func setSnodePool(to newValue: Set<Snode>, using transaction: Any? = nil) {
private static func setSnodePool(to newValue: Set<Legacy.Snode>, using transaction: Any? = nil) {
snodePool = newValue
let storage = SNSnodeKitConfiguration.shared.storage
if let transaction = transaction {
@ -84,7 +84,7 @@ public final class SnodeAPI : NSObject {
}
}
private static func dropSnodeFromSnodePool(_ snode: Snode) {
private static func dropSnodeFromSnodePool(_ snode: Legacy.Snode) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -107,7 +107,7 @@ public final class SnodeAPI : NSObject {
loadedSwarms.insert(publicKey)
}
private static func setSwarm(to newValue: Set<Snode>, for publicKey: String, persist: Bool = true) {
private static func setSwarm(to newValue: Set<Legacy.Snode>, for publicKey: String, persist: Bool = true) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -118,7 +118,7 @@ public final class SnodeAPI : NSObject {
}
}
public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
public static func dropSnodeFromSwarmIfNeeded(_ snode: Legacy.Snode, publicKey: String) {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif
@ -129,7 +129,7 @@ public final class SnodeAPI : NSObject {
}
// MARK: Internal API
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise {
internal static func invoke(_ method: Legacy.Snode.Method, on snode: Legacy.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 {
@ -141,7 +141,7 @@ public final class SnodeAPI : NSObject {
}
}
private static func getNetworkTime(from snode: Snode) -> Promise<UInt64> {
private static func getNetworkTime(from snode: Legacy.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,12 +149,12 @@ public final class SnodeAPI : NSObject {
}
}
internal static func getRandomSnode() -> Promise<Snode> {
internal static func getRandomSnode() -> Promise<Legacy.Snode> {
// randomElement() uses the system's default random generator, which is cryptographically secure
return getSnodePool().map2 { $0.randomElement()! }
}
private static func getSnodePoolFromSeedNode() -> Promise<Set<Snode>> {
private static func getSnodePoolFromSeedNode() -> Promise<Set<Legacy.Snode>> {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
@ -168,10 +168,10 @@ public final class SnodeAPI : NSObject {
]
]
SNLog("Populating snode pool using seed node: \(target).")
let (promise, seal) = Promise<Set<Snode>>.pending()
let (promise, seal) = Promise<Set<Legacy.Snode>>.pending()
Threading.workQueue.async {
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Set<Snode> in
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,
@ -179,7 +179,7 @@ public final class SnodeAPI : NSObject {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
}
}.done2 { snodePool in
@ -193,15 +193,15 @@ public final class SnodeAPI : NSObject {
return promise
}
private static func getSnodePoolFromSnode() -> Promise<Set<Snode>> {
private static func getSnodePoolFromSnode() -> Promise<Set<Legacy.Snode>> {
var snodePool = SnodeAPI.snodePool
var snodes: Set<Snode> = []
var snodes: Set<Legacy.Snode> = []
(0..<3).forEach { _ in
let snode = snodePool.randomElement()!
snodePool.remove(snode)
snodes.insert(snode)
}
let snodePoolPromises: [Promise<Set<Snode>>] = snodes.map { snode in
let snodePoolPromises: [Promise<Set<Legacy.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
@ -226,18 +226,18 @@ public final class SnodeAPI : NSObject {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
}
}
}
let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set<Snode> in
var result: Set<Snode> = results[0]
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([Snode](result)[0..<256]) : result
return (result.count > 256) ? Set([Legacy.Snode](result)[0..<256]) : result
} else {
throw Error.inconsistentSnodePools
}
@ -251,7 +251,7 @@ public final class SnodeAPI : NSObject {
AnyPromise.from(getSnodePool())
}
public static func getSnodePool() -> Promise<Set<Snode>> {
public static func getSnodePool() -> Promise<Set<Legacy.Snode>> {
loadSnodePoolIfNeeded()
let now = Date()
let hasSnodePoolExpired = given(Storage.shared.getLastSnodePoolRefreshDate()) { now.timeIntervalSince($0) > 2 * 60 * 60 } ?? true
@ -259,7 +259,7 @@ public final class SnodeAPI : NSObject {
let hasInsufficientSnodes = (snodePool.count < minSnodePoolCount)
if hasInsufficientSnodes || hasSnodePoolExpired {
if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise }
let promise: Promise<Set<Snode>>
let promise: Promise<Set<Legacy.Snode>>
if snodePool.count < minSnodePoolCount {
promise = getSnodePoolFromSeedNode()
} else {
@ -268,15 +268,15 @@ public final class SnodeAPI : NSObject {
}
}
getSnodePoolPromise = promise
promise.map2 { snodePool -> Set<Snode> in
promise.map2 { snodePool -> Set<Legacy.Snode> in
if snodePool.isEmpty {
throw Error.snodePoolUpdatingFailed
} else {
return snodePool
}
}
promise.then2 { snodePool -> Promise<Set<Snode>> in
let (promise, seal) = Promise<Set<Snode>>.pending()
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)
@ -366,15 +366,15 @@ public final class SnodeAPI : NSObject {
return promise
}
public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
public static func getTargetSnodes(for publicKey: String) -> Promise<[Legacy.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<Snode>> {
public static func getSwarm(for publicKey: String) -> Promise<Set<Legacy.Snode>> {
loadSwarmIfNeeded(for: publicKey)
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minSwarmSnodeCount {
return Promise<Set<Snode>> { $0.fulfill(cachedSwarm) }
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 ]
@ -390,7 +390,7 @@ public final class SnodeAPI : NSObject {
}
}
public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
public static func getRawMessages(from snode: Legacy.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) }
@ -412,7 +412,7 @@ public final class SnodeAPI : NSObject {
return promise
}
private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
private static func getMessagesInternal(from snode: Legacy.Snode, associatedWith publicKey: String) -> RawResponsePromise {
let storage = SNSnodeKitConfiguration.shared.storage
// NOTE: All authentication logic is currently commented out, the reason being that we can't currently support
@ -468,7 +468,7 @@ public final class SnodeAPI : NSObject {
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getSwarm(for: publicKey).then2 { swarm -> Promise<[String:Bool]> in
let snode = swarm.randomElement()!
let verificationData = (Snode.Method.deleteMessage.rawValue + serverHashes.joined(separator: "")).data(using: String.Encoding.utf8)!
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,
@ -515,7 +515,7 @@ public final class SnodeAPI : NSObject {
let snode = swarm.randomElement()!
return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
getNetworkTime(from: snode).then2 { timestamp -> Promise<[String:Bool]> in
let verificationData = (Snode.Method.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)!
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 parameters: JSON = [
"pubkey" : userX25519PublicKey,
@ -558,7 +558,7 @@ 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<Snode> {
private static func parseSnodes(from rawResponse: Any) -> Set<Legacy.Snode> {
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
SNLog("Failed to parse snodes from: \(rawResponse).")
return []
@ -569,17 +569,17 @@ public final class SnodeAPI : NSObject {
SNLog("Failed to parse snode from: \(rawSnode).")
return nil
}
return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
return Legacy.Snode(address: "https://\(address)", port: port, publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
}
public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [JSON] {
public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Legacy.Snode, associatedWith publicKey: String) -> [JSON] {
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages)
return removeDuplicates(from: rawMessages, associatedWith: publicKey)
}
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
private static func updateLastMessageHashValueIfPossible(for snode: Legacy.Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
if let lastMessage = rawMessages.last, 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,
@ -614,7 +614,7 @@ public final class SnodeAPI : NSObject {
// 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: Snode, associatedWith publicKey: String? = nil) -> Error? {
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Legacy.Snode, associatedWith publicKey: String? = nil) -> Error? {
#if DEBUG
dispatchPrecondition(condition: .onQueue(Threading.workQueue))
#endif

View File

@ -7,18 +7,18 @@ extension Storage {
private static let snodePoolCollection = "LokiSnodePoolCollection"
private static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection"
public func getSnodePool() -> Set<Snode> {
var result: Set<Snode> = []
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? Snode else { return }
guard let snode = object as? Legacy.Snode else { return }
result.insert(snode)
}
}
return result
}
public func setSnodePool(to snodePool: Set<Snode>, using transaction: Any) {
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)
@ -49,20 +49,21 @@ extension Storage {
return "LokiSwarmCollection-\(publicKey)"
}
public func getSwarm(for publicKey: String) -> Set<Snode> {
var result: Set<Snode> = []
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? Snode else { return }
guard let snode = object as? Legacy.Snode else { return }
result.insert(snode)
}
}
return result
}
public func setSwarm(to swarm: Set<Snode>, for publicKey: String, using transaction: Any) {
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)
@ -80,7 +81,7 @@ extension Storage {
private static let lastMessageHashCollection = "LokiLastMessageHashCollection"
public func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? {
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
@ -93,17 +94,17 @@ extension Storage {
return result
}
public func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? {
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: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) {
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: Snode, associatedWith publicKey: String) {
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()
@ -114,7 +115,7 @@ extension Storage {
}
}
public func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: Any) {
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)
}

View File

@ -14,15 +14,15 @@ public protocol SessionSnodeKitStorageProtocol {
func getUserED25519KeyPair() -> Box.KeyPair?
func getOnionRequestPaths() -> [OnionRequestAPI.Path]
func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any)
func getSnodePool() -> Set<Snode>
func setSnodePool(to snodePool: Set<Snode>, 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<Snode>
func setSwarm(to swarm: Set<Snode>, for publicKey: String, using transaction: Any)
func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String?
func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any)
func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String)
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,224 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SignalCoreKit
enum GRDBStorageError: Error { // TODO: Rename to `StorageError`
case invalidKeySpec
}
// TODO: Protocol for storage (just need to have 'read' and 'write' methods and mock 'Database'?
// TODO: Rename to `Storage`
public final class GRDBStorage {
public static var shared: GRDBStorage! // TODO: Figure out how/if we want to do this
private static let dbFileName: String = "Session.sqlite"
private static let keychainService: String = "TSKeyChainService"
private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec"
private static let kSQLCipherKeySpecLength: Int32 = 48
private static var sharedDatabaseDirectoryPath: String { "\(OWSFileSystem.appSharedDataDirectoryPath())/database" }
private static var databasePath: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)" }
private static var databasePathShm: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)-shm" }
private static var databasePathWal: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)-wal" }
private let dbPool: DatabasePool
private let migrator: DatabaseMigrator
// MARK: - Initialization
public init?(
migrations: [TargetMigrations]
) throws {
print("RAWR START \("\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)")")
GRDBStorage.deleteDatabaseFiles() // TODO: Remove this
try! GRDBStorage.deleteDbKeys() // TODO: Remove this
// Create the database directory if needed and ensure it's protection level is set before attempting to
// create the database KeySpec or the database itself
OWSFileSystem.ensureDirectoryExists(GRDBStorage.sharedDatabaseDirectoryPath)
OWSFileSystem.protectFileOrFolder(atPath: GRDBStorage.sharedDatabaseDirectoryPath)
// Generate the database KeySpec if needed (this MUST be done before we try to access the database
// as a different thread might attempt to access the database before the key is successfully created)
//
// Note: We reset the bytes immediately after generation to ensure the database key doesn't hang
// around in memory unintentionally
var tmpKeySpec: Data = GRDBStorage.getOrGenerateDatabaseKeySpec()
tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count)
// Configure the database and create the DatabasePool for interacting with the database
var config = Configuration()
config.maximumReaderCount = 10 // Increase the max read connection limit - Default is 5
config.prepareDatabase { db in
var keySpec: Data = GRDBStorage.getOrGenerateDatabaseKeySpec()
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
// Use a raw key spec, where the 96 hexadecimal digits are provided
// (i.e. 64 hex for the 256 bit key, followed by 32 hex for the 128 bit salt)
// using explicit BLOB syntax, e.g.:
//
// x'98483C6EB40B6C31A448C22A66DED3B5E5E8D5119CAC8327B655C8B5C483648101010101010101010101010101010101'
keySpec = try (keySpec.toHexString().data(using: .utf8) ?? { throw GRDBStorageError.invalidKeySpec }())
keySpec.insert(contentsOf: [120, 39], at: 0) // "x'" prefix
keySpec.append(39) // "'" suffix
try db.usePassphrase(keySpec)
// According to the SQLCipher docs iOS needs the 'cipher_plaintext_header_size' value set to at least
// 32 as iOS extends special privileges to the database and needs this header to be in plaintext
// to determine the file type
//
// For more info see: https://www.zetetic.net/sqlcipher/sqlcipher-api/#cipher_plaintext_header_size
try db.execute(sql: "PRAGMA cipher_plaintext_header_size = 32")
}
// Create the DatabasePool to allow us to connect to the database
dbPool = try DatabasePool(
path: "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)",
configuration: config
)
// Setup and run any required migrations
migrator = {
var migrator: DatabaseMigrator = DatabaseMigrator()
migrations
.sorted()
.reduce(into: [[(identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet)]]()) { result, next in
next.migrations.enumerated().forEach { index, migrationSet in
if result.count <= index {
result.append([])
}
result[index] = (result[index] + [(next.identifier, migrationSet)])
}
}
.compactMap { $0 }
.forEach { sortedMigrationInfo in
sortedMigrationInfo.forEach { migrationInfo in
migrationInfo.migrations.forEach { migration in
migrator.registerMigration(migrationInfo.identifier, migration: migration)
}
}
}
return migrator
}()
try! migrator.migrate(dbPool)
GRDBStorage.shared = self // TODO: Fix this
}
// MARK: - Security
private static func getDatabaseCipherKeySpec() throws -> Data {
return try CurrentAppContext().keychainStorage().data(forService: keychainService, key: dbCipherKeySpecKey)
}
@discardableResult private static func getOrGenerateDatabaseKeySpec() -> Data {
do {
var keySpec: Data = try getDatabaseCipherKeySpec()
defer { keySpec.resetBytes(in: 0..<keySpec.count) }
guard keySpec.count == kSQLCipherKeySpecLength else { throw GRDBStorageError.invalidKeySpec }
return keySpec
}
catch {
print("RAWR \(error.localizedDescription), \((error as? KeychainStorageError)?.code), \(errSecItemNotFound)")
switch (error, (error as? KeychainStorageError)?.code) {
// TODO: Are there other errors we know about that indicate an invalid keychain?
// errSecNotAvailable: OSStatus { get } /* No keychain is available. You may need to restart your computer. */
// public var errSecNoSuchKeychain
//errSecInteractionNotAllowed
case (GRDBStorageError.invalidKeySpec, _):
// For these cases it means either the keySpec or the keychain has become corrupt so in order to
// get back to a "known good state" and behave like a new install we need to reset the storage
// and regenerate the key
// TODO: Check what this 'isRunningTests' does (use the approach to check if XCTTestCase exists instead?)
if !CurrentAppContext().isRunningTests {
// Try to reset app by deleting database.
resetAllStorage()
}
fallthrough
case (_, errSecItemNotFound):
// No keySpec was found so we need to generate a new one
do {
var keySpec: Data = Randomness.generateRandomBytes(kSQLCipherKeySpecLength)
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
try CurrentAppContext().keychainStorage().set(data: keySpec, service: keychainService, key: dbCipherKeySpecKey)
print("RAWR new keySpec generated and saved")
return keySpec
}
catch {
Thread.sleep(forTimeInterval: 15) // Sleep to allow any background behaviours to complete
fatalError("Setting keychain value failed with error: \(error.localizedDescription)")
}
default:
// Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, the keychain will be inaccessible
// after device restart until device is unlocked for the first time. If the app receives a push
// notification, we won't be able to access the keychain to process that notification, so we should
// just terminate by throwing an uncaught exception
if CurrentAppContext().isMainApp || CurrentAppContext().isInBackground() {
let appState: UIApplication.State = CurrentAppContext().reportedApplicationState
// In this case we should have already detected the situation earlier and exited gracefully (in the
// app delegate) using isDatabasePasswordAccessible, but we want to stop the app running here anyway
Thread.sleep(forTimeInterval: 5) // Sleep to allow any background behaviours to complete
fatalError("CipherKeySpec inaccessible. New install or no unlock since device restart?, ApplicationState: \(NSStringForUIApplicationState(appState))")
}
Thread.sleep(forTimeInterval: 5) // Sleep to allow any background behaviours to complete
fatalError("CipherKeySpec inaccessible; not main app.")
}
}
}
// MARK: - File Management
private static func resetAllStorage() {
NotificationCenter.default.post(name: .resetStorage, object: nil)
// This might be redundant but in the spirit of thoroughness...
self.deleteDatabaseFiles()
try? self.deleteDbKeys()
if CurrentAppContext().isMainApp {
// TSAttachmentStream.deleteAttachments()
}
// TODO: Delete Profiles on Disk?
}
private static func deleteDatabaseFiles() {
OWSFileSystem.deleteFile(databasePath)
OWSFileSystem.deleteFile(databasePathShm)
OWSFileSystem.deleteFile(databasePathWal)
}
private static func deleteDbKeys() throws {
try CurrentAppContext().keychainStorage().remove(service: keychainService, key: dbCipherKeySpecKey)
}
// MARK: - Functions
public func write<T>(updates: (Database) throws -> T) throws -> 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)
}
}

View File

@ -6,12 +6,18 @@ import Foundation
import SAMKeychain
public enum KeychainStorageError: Error {
case failure(description: String)
case failure(code: Int32?, description: String)
public var code: Int32? {
switch self {
case .failure(let code, _): return code
}
}
}
// MARK: -
@objc public protocol SSKKeychainStorage: class {
@objc public protocol SSKKeychainStorage: AnyObject {
@objc func string(forService service: String, key: String) throws -> String
@ -40,10 +46,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage {
var error: NSError?
let result = SAMKeychain.password(forService: service, account: key, error: &error)
if let error = error {
throw KeychainStorageError.failure(description: "\(logTag) error retrieving string: \(error)")
throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving string: \(error)")
}
guard let string = result else {
throw KeychainStorageError.failure(description: "\(logTag) could not retrieve string")
throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve string")
}
return string
}
@ -55,10 +61,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage {
var error: NSError?
let result = SAMKeychain.setPassword(string, forService: service, account: key, error: &error)
if let error = error {
throw KeychainStorageError.failure(description: "\(logTag) error setting string: \(error)")
throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting string: \(error)")
}
guard result else {
throw KeychainStorageError.failure(description: "\(logTag) could not set string")
throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set string")
}
}
@ -66,10 +72,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage {
var error: NSError?
let result = SAMKeychain.passwordData(forService: service, account: key, error: &error)
if let error = error {
throw KeychainStorageError.failure(description: "\(logTag) error retrieving data: \(error)")
throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving data: \(error)")
}
guard let data = result else {
throw KeychainStorageError.failure(description: "\(logTag) could not retrieve data")
throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve data")
}
return data
}
@ -81,10 +87,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage {
var error: NSError?
let result = SAMKeychain.setPasswordData(data, forService: service, account: key, error: &error)
if let error = error {
throw KeychainStorageError.failure(description: "\(logTag) error setting data: \(error)")
throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting data: \(error)")
}
guard result else {
throw KeychainStorageError.failure(description: "\(logTag) could not set data")
throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set data")
}
}
@ -96,10 +102,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage {
if error.code == errSecItemNotFound {
return
}
throw KeychainStorageError.failure(description: "\(logTag) error removing data: \(error)")
throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error removing data: \(error)")
}
guard result else {
throw KeychainStorageError.failure(description: "\(logTag) could not remove data")
throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not remove data")
}
}
}

View File

@ -0,0 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public protocol ColumnExpressible {
associatedtype Columns: ColumnExpression
}

View File

@ -0,0 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public protocol Migration {
static var identifier: String { get }
static func migrate(_ db: Database) throws
}

View File

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

View File

@ -0,0 +1,59 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public struct TargetMigrations: Comparable {
/// This identifier is used to determine the order each set of migrations should run in.
///
/// All migrations within a specific set will run first, followed by all migrations for the same set index in
/// the next `Identifier` before moving on to the next `MigrationSet`. So given the migrations:
///
/// `{a: [1], [2, 3]}, {b: [4, 5], [6]}`
///
/// the migrations will run in the following order:
///
/// `a1, b4, b5, a2, a3, b6`
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 snodeKit
case messagingKit
public static func < (lhs: Self, rhs: Self) -> Bool {
let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count)
let rhsIndex: Int = (Identifier.allCases.firstIndex(of: rhs) ?? Identifier.allCases.count)
return (lhsIndex < rhsIndex)
}
}
public typealias MigrationSet = [Migration.Type]
let identifier: Identifier
let migrations: [MigrationSet]
// MARK: - Initialization
public init(
identifier: Identifier,
migrations: [MigrationSet]
) {
self.identifier = identifier
self.migrations = migrations
}
// MARK: - Equatable
public static func == (lhs: TargetMigrations, rhs: TargetMigrations) -> Bool {
return (
lhs.identifier == rhs.identifier &&
lhs.migrations.count == rhs.migrations.count
)
}
// MARK: - Comparable
public static func < (lhs: Self, rhs: Self) -> Bool {
return (lhs.identifier < rhs.identifier)
}
}

View File

@ -0,0 +1,33 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
/// This is a convenience wrapper around the GRDB `TableDefinition` class which allows for shorthand
/// when creating tables
public class TypedTableDefinition<T> where T: TableRecord, T: ColumnExpressible {
let definition: TableDefinition
init(definition: TableDefinition) {
self.definition = definition
}
@discardableResult public func column(_ key: T.Columns, _ type: Database.ColumnType? = nil) -> ColumnDefinition {
return definition.column(key.name, type)
}
public func primaryKey(_ columns: [T.Columns], onConflict: Database.ConflictResolution? = nil) {
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 {
return definition.foreignKey(
columns.map { $0.name },
references: table.databaseTableName,
columns: destinationColumns?.map { $0.name },
onDelete: onDelete,
onUpdate: onUpdate,
deferred: deferred
)
}
}

View File

@ -0,0 +1,22 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension ColumnDefinition {
@discardableResult func references<T>(
_ table: T.Type,
column: T.Columns? = nil,
onDelete deleteAction: Database.ForeignKeyAction? = nil,
onUpdate updateAction: Database.ForeignKeyAction? = nil,
deferred: Bool = false
) -> Self where T: TableRecord, T: ColumnExpressible {
return references(
T.databaseTableName,
column: column?.name,
onDelete: deleteAction,
onUpdate: updateAction,
deferred: deferred
)
}
}

View File

@ -0,0 +1,18 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension Database {
func create<T>(
table: T.Type,
options: TableOptions = [],
body: (TypedTableDefinition<T>) throws -> Void
) throws where T: TableRecord, T: ColumnExpressible {
try create(table: T.databaseTableName, options: options) { tableDefinition in
let typedDefinition: TypedTableDefinition<T> = TypedTableDefinition(definition: tableDefinition)
try body(typedDefinition)
}
}
}

View File

@ -0,0 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension DatabaseMigrator {
mutating func registerMigration(_ identifier: TargetMigrations.Identifier, migration: Migration.Type, foreignKeyChecks: ForeignKeyChecks = .deferred) {
self.registerMigration("\(identifier).\(migration.identifier)", migrate: migration.migrate)
}
}

View File

@ -0,0 +1,11 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension Notification.Name {
static let resetStorage = Notification.Name("resetStorage")
}
@objc public extension NSNotification {
@objc static let resetStorage = Notification.Name.resetStorage.rawValue as NSString
}

View File

@ -0,0 +1,19 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension Set {
func inserting(_ value: Element) -> Set<Element> {
var updatedSet: Set<Element> = self
updatedSet.insert(value)
return updatedSet
}
func removing(_ value: Element) -> Set<Element> {
var updatedSet: Set<Element> = self
updatedSet.remove(value)
return updatedSet
}
}

View File

@ -3,12 +3,34 @@ import SessionSnodeKit
extension OWSPrimaryStorage : OWSPrimaryStorageProtocol { }
var isSetup: Bool = false // TODO: Remove this
@objc(SNConfiguration)
public final class Configuration : NSObject {
@objc public static func performMainSetup() {
// Need to do this first to ensure the legacy database exists
SNUtilitiesKit.configure(
owsPrimaryStorage: OWSPrimaryStorage.shared(),
maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier)
)
if !isSetup {
isSetup = true
// TODO: Need to store this result somewhere?
// TODO: This function seems to get called multiple times
//DispatchQueue.main.once
let storage: GRDBStorage? = try? GRDBStorage(
migrations: [
SNSnodeKit.migrations(),
SNMessagingKit.migrations()
]
)
}
SNMessagingKit.configure(storage: Storage.shared)
SNSnodeKit.configure(storage: Storage.shared)
SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier))
}
}