Fixed the broken tests

Cleaned up the Dependencies so that tests can run synchronously without having to custom set queues as much
Sorted out the crypto and network dependencies to avoid needing weird dependency inheritance
Fixed the flaky tests so they are no longer flaky
Fixed some unexpected JobRunner behaviours
Updated the CI config to use a local build directory for derivedData (now works with build tweaks)
This commit is contained in:
Morgan Pretty 2023-08-01 14:27:41 +10:00
parent e768bebe6d
commit a41f1c1366
139 changed files with 7211 additions and 7867 deletions

View File

@ -71,7 +71,7 @@ local update_cocoapods_cache = {
name: 'Run Unit Tests',
commands: [
'mkdir build',
'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14" -destination "platform=iOS Simulator,name=iPhone 14 Pro Max" -parallel-testing-enabled YES -test-timeouts-enabled YES -maximum-test-execution-time-allowance 2 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci --report junit --report-path ./build/reports --junit-report-filename junit2.xml'
'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -destination "platform=iOS Simulator,name=iPhone 14" -destination "platform=iOS Simulator,name=iPhone 14 Pro Max" -parallel-testing-enabled YES -test-timeouts-enabled YES -maximum-test-execution-time-allowance 2 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify --is-ci --report junit --report-path ./build/reports --junit-report-filename junit2.xml'
],
},
update_cocoapods_cache
@ -91,7 +91,7 @@ local update_cocoapods_cache = {
name: 'Build',
commands: [
'mkdir build',
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | ./Pods/xcbeautify/xcbeautify --is-ci'
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | ./Pods/xcbeautify/xcbeautify --is-ci'
],
},
update_cocoapods_cache,
@ -118,7 +118,7 @@ local update_cocoapods_cache = {
name: 'Build',
commands: [
'mkdir build',
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphoneos -archivePath ./build/Session.xcarchive -destination "generic/platform=iOS" -allowProvisioningUpdates'
'xcodebuild archive -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -configuration "App Store Release" -sdk iphoneos -archivePath ./build/Session.xcarchive -destination "generic/platform=iOS" -allowProvisioningUpdates CODE_SIGNING_ALLOWED=NO'
],
},
update_cocoapods_cache,

View File

@ -28,13 +28,13 @@ else
base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}"
fi
mkdir -v "$base"
mkdir -vp "$base"
# Copy over the build products
prod_path="build/Session.xcarchive"
sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app"
mkdir build
mkdir -p build
echo "Test" > "build/test.txt"
if [ ! -d $prod_path ]; then

View File

@ -470,11 +470,10 @@
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4727E02561000769AF /* CommonMockedExtensions.swift */; };
FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4C27E17156000769AF /* MockOGMCache.swift */; };
FD078E4F27E175F1000769AF /* DependencyExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E4E27E175F1000769AF /* DependencyExtensions.swift */; };
FD078E5227E1760A000769AF /* OGMDependencyExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5127E1760A000769AF /* OGMDependencyExtensions.swift */; };
FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */; };
FD078E5A27E29F09000769AF /* MockNonce16Generator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5927E29F09000769AF /* MockNonce16Generator.swift */; };
FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */; };
FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; };
FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; };
FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0969F82A69FFE700C5C365 /* Mocked.swift */; };
FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; };
FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; };
FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; };
@ -532,6 +531,21 @@
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; };
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; };
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1A2A651E6D0000B97C /* NetworkType.swift */; };
FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE1E2A65269C0000B97C /* Crypto.swift */; };
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */; };
FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */; };
FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */; };
FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; };
FD23CE292A6775650000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; };
FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE272A67755C0000B97C /* MockCrypto.swift */; };
FD23CE2C2A678DF80000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; };
FD23CE2D2A678E1E0000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; };
FD23CE2E2A678E1E0000B97C /* MockCaches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2B2A678DF80000B97C /* MockCaches.swift */; };
FD23CE302A67B8820000B97C /* Caches.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE2F2A67B8820000B97C /* Caches.swift */; };
FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; };
FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; };
FD23CE352A67C4DA0000B97C /* MockNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD23CE312A67C38D0000B97C /* MockNetwork.swift */; };
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
@ -615,9 +629,8 @@
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; };
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; };
FD3C906A27E417CE00CD579F /* CryptoSMKSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */; };
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; };
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906E27E43E8700CD579F /* MockBox.swift */; };
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */; };
FD3E0C84283B5835002A425C /* SessionThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */; };
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */; };
@ -634,6 +647,7 @@
FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; };
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; };
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; };
FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; };
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; };
FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; };
FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; };
@ -696,7 +710,6 @@
FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */; };
FD7728962849E7E90018502F /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728952849E7E90018502F /* String+Utilities.swift */; };
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; };
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD772899284AF1BD0018502F /* Sodium+Utilities.swift */; };
FD77289E284EF1C50018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289D284EF1C50018502F /* Sodium+Utilities.swift */; };
FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */; };
FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
@ -708,6 +721,7 @@
FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; };
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; };
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9D127D59495005E1583 /* MockUserDefaults.swift */; };
FD83DCDD2A739D350065FFAE /* RetryWithDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */; };
FD848B8B283DC509000E298B /* PagedDatabaseObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */; };
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8C283E0B26000E298B /* MessageInputTypes.swift */; };
FD848B8F283EF2A8000E298B /* UIScrollView+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B8E283EF2A8000E298B /* UIScrollView+Utilities.swift */; };
@ -716,11 +730,6 @@
FD848B9828422F1A000E298B /* Date+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9728422F1A000E298B /* Date+Utilities.swift */; };
FD848B9A28442CE6000E298B /* StorageError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9928442CE6000E298B /* StorageError.swift */; };
FD848B9C284435D7000E298B /* AppSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD848B9B284435D7000E298B /* AppSetup.swift */; };
FD859EF427C2F49200510D0C /* MockSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* MockSodium.swift */; };
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* MockSign.swift */; };
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */; };
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF927C2F5C500510D0C /* MockGenericHash.swift */; };
FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFB27C2F60700510D0C /* MockEd25519.swift */; };
FD87DCFA28B74DB300AF0F98 /* ConversationSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */; };
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; };
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; };
@ -745,6 +754,9 @@
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9DD2702A72516D00ECB68E /* TestExtensions.swift */; };
FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */; };
FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */; };
FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDA1E83A29A5F2D500C5C3BD /* SessionUtil+Shared.swift */; };
@ -769,14 +781,11 @@
FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */; };
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */; };
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */; };
FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */; };
FDC290A627D860CE005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */; };
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */; };
FDC4380927B31D4E00C60D73 /* OpenGroupAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */; };
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; };
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; };
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; };
FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */; };
@ -796,8 +805,6 @@
FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A527BB113A00C60D73 /* UserUnbanRequest.swift */; };
FDC438AA27BB12BB00C60D73 /* UserModeratorRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438A927BB12BB00C60D73 /* UserModeratorRequest.swift */; };
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438BC27BB2AB400C60D73 /* Mockable.swift */; };
FDC438C127BB4E6800C60D73 /* SMKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */; };
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C227BB512200C60D73 /* SodiumProtocols.swift */; };
FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C627BB6DF000C60D73 /* DirectMessage.swift */; };
FDC438C927BB706500C60D73 /* SendDirectMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */; };
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */; };
@ -816,8 +823,8 @@
FDDCBDAB29E776BF00303C38 /* seed2-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */; };
FDDCBDAC29E776BF00303C38 /* seed3-2023-2y.crt in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */; };
FDDCBDAD29E776BF00303C38 /* seed3-2023-2y.der in Resources */ = {isa = PBXBuildFile; fileRef = FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */; };
FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; };
FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; };
FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */; };
FDE658A129418C7900A33BC1 /* CryptoKit+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */; };
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE658A229418E2F00A33BC1 /* KeyPair.swift */; };
FDE6E99829F8E63A00F93C5D /* Accessibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE6E99729F8E63A00F93C5D /* Accessibility.swift */; };
@ -852,7 +859,6 @@
FDF8488629405A61007DCAE5 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488529405A60007DCAE5 /* Request.swift */; };
FDF8488829405A9A007DCAE5 /* SOGSBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488729405A9A007DCAE5 /* SOGSBatchRequest.swift */; };
FDF8488929405B27007DCAE5 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */; };
FDF8488B29405BF2007DCAE5 /* SSKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */; };
FDF8488E29405C04007DCAE5 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8488D29405C04007DCAE5 /* GetSnodePoolJob.swift */; };
FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */; };
FDF8489429405C1B007DCAE5 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */; };
@ -911,6 +917,7 @@
FDFDE128282D05530098B17F /* MediaPresentationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE127282D05530098B17F /* MediaPresentationContext.swift */; };
FDFDE12A282D056B0098B17F /* MediaZoomAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */; };
FDFF61D729F2600300F95FB0 /* Identity+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */; };
FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */; };
FE5FDED6D91BB4B3FA5C104D /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A9C113D2086D3C8A68A371C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionShareExtension.framework */; };
/* End PBXBuildFile section */
@ -1626,10 +1633,7 @@
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonMockedExtensions.swift; sourceTree = "<group>"; };
FD078E4C27E17156000769AF /* MockOGMCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOGMCache.swift; sourceTree = "<group>"; };
FD078E4E27E175F1000769AF /* DependencyExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyExtensions.swift; sourceTree = "<group>"; };
FD078E5127E1760A000769AF /* OGMDependencyExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OGMDependencyExtensions.swift; sourceTree = "<group>"; };
FD078E5927E29F09000769AF /* MockNonce16Generator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNonce16Generator.swift; sourceTree = "<group>"; };
FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNonce24Generator.swift; sourceTree = "<group>"; };
FD0969F82A69FFE700C5C365 /* Mocked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mocked.swift; sourceTree = "<group>"; };
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
@ -1685,6 +1689,15 @@
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = "<group>"; };
FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = "<group>"; };
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = "<group>"; };
FD23CE1E2A65269C0000B97C /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = "<group>"; };
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupAPI+Crypto.swift"; sourceTree = "<group>"; };
FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionMessagingKit.swift"; sourceTree = "<group>"; };
FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependenciesSpec.swift; sourceTree = "<group>"; };
FD23CE272A67755C0000B97C /* MockCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCrypto.swift; sourceTree = "<group>"; };
FD23CE2B2A678DF80000B97C /* MockCaches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockCaches.swift; sourceTree = "<group>"; };
FD23CE2F2A67B8820000B97C /* Caches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Caches.swift; sourceTree = "<group>"; };
FD23CE312A67C38D0000B97C /* MockNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetwork.swift; sourceTree = "<group>"; };
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD28A4F527EAD44C00FF65E7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
@ -1736,9 +1749,8 @@
FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfoSpec.swift; sourceTree = "<group>"; };
FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponseSpec.swift; sourceTree = "<group>"; };
FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdLookupSpec.swift; sourceTree = "<group>"; };
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumUtilitiesSpec.swift; sourceTree = "<group>"; };
FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoSMKSpec.swift; sourceTree = "<group>"; };
FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderEncryptionSpec.swift; sourceTree = "<group>"; };
FD3C906E27E43E8700CD579F /* MockBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBox.swift; sourceTree = "<group>"; };
FD3C907027E445E500CD579F /* MessageReceiverDecryptionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageReceiverDecryptionSpec.swift; sourceTree = "<group>"; };
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupServerIdLookup.swift; sourceTree = "<group>"; };
FD3E0C83283B5835002A425C /* SessionThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModel.swift; sourceTree = "<group>"; };
@ -1754,6 +1766,7 @@
FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = "<group>"; };
FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = "<group>"; };
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; };
FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = "<group>"; };
FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = "<group>"; };
FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = "<group>"; };
@ -1811,7 +1824,6 @@
FD7692F62A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionThreadViewModelSpec.swift; sourceTree = "<group>"; };
FD7728952849E7E90018502F /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = "<group>"; };
FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
FD77289D284EF1C50018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
FD778B6329B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _014_GenerateInitialUserConfigDumps.swift; sourceTree = "<group>"; };
FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionUtilitiesKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -1822,6 +1834,7 @@
FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = "<group>"; };
FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = "<group>"; };
FD83B9D127D59495005E1583 /* MockUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDefaults.swift; sourceTree = "<group>"; };
FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryWithDependencies.swift; sourceTree = "<group>"; };
FD848B86283B844B000E298B /* MessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewModel.swift; sourceTree = "<group>"; };
FD848B8A283DC509000E298B /* PagedDatabaseObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedDatabaseObserver.swift; sourceTree = "<group>"; };
FD848B8C283E0B26000E298B /* MessageInputTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageInputTypes.swift; sourceTree = "<group>"; };
@ -1833,11 +1846,6 @@
FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = "<group>"; };
FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = "<group>"; };
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
FD859EF327C2F49200510D0C /* MockSodium.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSodium.swift; sourceTree = "<group>"; };
FD859EF527C2F52C00510D0C /* MockSign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSign.swift; sourceTree = "<group>"; };
FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAeadXChaCha20Poly1305Ietf.swift; sourceTree = "<group>"; };
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGenericHash.swift; sourceTree = "<group>"; };
FD859EFB27C2F60700510D0C /* MockEd25519.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEd25519.swift; sourceTree = "<group>"; };
FD87DCF928B74DB300AF0F98 /* ConversationSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSettingsViewModel.swift; sourceTree = "<group>"; };
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = "<group>"; };
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = "<group>"; };
@ -1858,6 +1866,7 @@
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; };
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
FDA1E83529A5748F00C5C3BD /* ConfigUserGroupsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserGroupsSpec.swift; sourceTree = "<group>"; };
FDA1E83829A5771A00C5C3BD /* LibSessionSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionSpec.swift; sourceTree = "<group>"; };
FDA1E83A29A5F2D500C5C3BD /* SessionUtil+Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+Shared.swift"; sourceTree = "<group>"; };
@ -1882,13 +1891,10 @@
FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSErrorSpec.swift; sourceTree = "<group>"; };
FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersonalizationSpec.swift; sourceTree = "<group>"; };
FDC2909927D71376005DAE71 /* NonceGeneratorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGeneratorSpec.swift; sourceTree = "<group>"; };
FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocolsSpec.swift; sourceTree = "<group>"; };
FDC2909D27D85751005DAE71 /* OpenGroupManagerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManagerSpec.swift; sourceTree = "<group>"; };
FDC290A527D860CE005DAE71 /* Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mock.swift; sourceTree = "<group>"; };
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NimbleExtensions.swift; sourceTree = "<group>"; };
FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestOnionRequestAPI.swift; sourceTree = "<group>"; };
FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPIError.swift; sourceTree = "<group>"; };
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = "<group>"; };
FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = "<group>"; };
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = "<group>"; };
FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushServerResponse.swift; sourceTree = "<group>"; };
@ -1910,8 +1916,6 @@
FDC438B027BB159600C60D73 /* RequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestInfo.swift; sourceTree = "<group>"; };
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseInfo.swift; sourceTree = "<group>"; };
FDC438BC27BB2AB400C60D73 /* Mockable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mockable.swift; sourceTree = "<group>"; };
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKDependencies.swift; sourceTree = "<group>"; };
FDC438C227BB512200C60D73 /* SodiumProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SodiumProtocols.swift; sourceTree = "<group>"; };
FDC438C627BB6DF000C60D73 /* DirectMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessage.swift; sourceTree = "<group>"; };
FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageRequest.swift; sourceTree = "<group>"; };
FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateMessageRequest.swift; sourceTree = "<group>"; };
@ -1929,8 +1933,8 @@
FDDCBDA529E776BF00303C38 /* seed2-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed2-2023-2y.der"; sourceTree = "<group>"; };
FDDCBDA629E776BF00303C38 /* seed3-2023-2y.crt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "seed3-2023-2y.crt"; sourceTree = "<group>"; };
FDDCBDA729E776BF00303C38 /* seed3-2023-2y.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = "seed3-2023-2y.der"; sourceTree = "<group>"; };
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = "<group>"; };
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = "<group>"; };
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JobRunnerSpec.swift; sourceTree = "<group>"; };
FDE658A029418C7900A33BC1 /* CryptoKit+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CryptoKit+Utilities.swift"; sourceTree = "<group>"; };
FDE658A229418E2F00A33BC1 /* KeyPair.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyPair.swift; sourceTree = "<group>"; };
FDE6E99729F8E63A00F93C5D /* Accessibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Accessibility.swift; sourceTree = "<group>"; };
@ -1968,7 +1972,6 @@
FDF8488229405A12007DCAE5 /* BatchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BatchResponse.swift; sourceTree = "<group>"; };
FDF8488529405A60007DCAE5 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = "<group>"; };
FDF8488729405A9A007DCAE5 /* SOGSBatchRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOGSBatchRequest.swift; sourceTree = "<group>"; };
FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKDependencies.swift; sourceTree = "<group>"; };
FDF8488D29405C04007DCAE5 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
FDF8489029405C13007DCAE5 /* SnodeAPINamespace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPINamespace.swift; sourceTree = "<group>"; };
FDF8489329405C1B007DCAE5 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = "<group>"; };
@ -2026,6 +2029,7 @@
FDFDE127282D05530098B17F /* MediaPresentationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPresentationContext.swift; sourceTree = "<group>"; };
FDFDE129282D056B0098B17F /* MediaZoomAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaZoomAnimationController.swift; sourceTree = "<group>"; };
FDFF61D629F2600300F95FB0 /* Identity+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Identity+Utilities.swift"; sourceTree = "<group>"; };
FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONEncoder+Utilities.swift"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -2556,6 +2560,7 @@
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */,
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */,
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */,
FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */,
);
path = Networking;
@ -2583,14 +2588,15 @@
isa = PBXGroup;
children = (
7BD477A727EC39F5004E2822 /* Atomic.swift */,
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */,
7BAF54D527ACD0E2003D12F8 /* ReusableView.swift */,
C33FDB8A255A581200E217F9 /* AppContext.h */,
C33FDB85255A581100E217F9 /* AppContext.m */,
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */,
FDC4383D27B4708600C60D73 /* Atomic.swift */,
FDC438CC27BC641200C60D73 /* Set+Utilities.swift */,
C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */,
B8F5F58225EC94A6003BF8D4 /* Collection+Utilities.swift */,
FD23CE2F2A67B8820000B97C /* Caches.swift */,
FDFD645A27F26D4600808CA1 /* Data+Utilities.swift */,
FDC6D75F2862B3F600B04575 /* Dependencies.swift */,
C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */,
@ -3180,6 +3186,7 @@
C3A721332558BDDF0043A11F /* Open Groups */ = {
isa = PBXGroup;
children = (
FD23CE202A661CE80000B97C /* Crypto */,
FDC4381827B34EAD00C60D73 /* Models */,
FDC4380727B31D3A00C60D73 /* Types */,
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */,
@ -3204,9 +3211,10 @@
children = (
C33FDB01255A580700E217F9 /* AppReadiness.h */,
C33FDB75255A581000E217F9 /* AppReadiness.m */,
FDF0B7542807C4BB004C14C5 /* Environment.swift */,
FD23CE232A675C440000B97C /* Crypto+SessionMessagingKit.swift */,
FD859EF127BF6BA200510D0C /* Data+Utilities.swift */,
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
FDF0B7542807C4BB004C14C5 /* Environment.swift */,
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
C3A71D0A2558989C0043A11F /* MessageWrapper.swift */,
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */,
@ -3220,7 +3228,6 @@
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */,
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */,
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */,
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */,
FD16AB602A1DD9B60083D849 /* ProfilePictureView+Convenience.swift */,
C3A3A170256E1D25004D228D /* SSKReachabilityManager.swift */,
C3ECBF7A257056B700EA7FCE /* Threading.swift */,
@ -3239,7 +3246,6 @@
FDF8488C29405C04007DCAE5 /* Jobs */,
FDF8489229405C1B007DCAE5 /* Networking */,
C3C2A5CD255385F300C340D1 /* Utilities */,
FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
);
path = SessionSnodeKit;
@ -3306,7 +3312,6 @@
FD8ECF7529340F4800C0D1BB /* SessionUtil */,
FD3E0C82283B581F002A425C /* Shared Models */,
C3BBE0B32554F0D30050F1E3 /* Utilities */,
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */,
FD245C612850664300B966DD /* Configuration.swift */,
);
path = SessionMessagingKit;
@ -3552,7 +3557,10 @@
FDA8EB0F280F8238002B68E5 /* Codable+Utilities.swift */,
FD3003692A3ADD6000B5A5FB /* CExceptionHelper.h */,
FD30036D2A3AE26000B5A5FB /* CExceptionHelper.mm */,
FD23CE1E2A65269C0000B97C /* Crypto.swift */,
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */,
FD09796A27F6C67500936362 /* Failable.swift */,
FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */,
FD09797127FAA2F500936362 /* Optional+Utilities.swift */,
FD09797C27FBDB2000936362 /* Notification+Utilities.swift */,
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */,
@ -3736,6 +3744,14 @@
path = Utilities;
sourceTree = "<group>";
};
FD23CE202A661CE80000B97C /* Crypto */ = {
isa = PBXGroup;
children = (
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */,
);
path = Crypto;
sourceTree = "<group>";
};
FD29598E2A43BE5400888A17 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -3838,7 +3854,7 @@
FD3C906827E417B100CD579F /* Utilities */ = {
isa = PBXGroup;
children = (
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */,
FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3876,6 +3892,7 @@
children = (
FD7115F628C8150D00B47552 /* Disposable Views */,
FD7115FD28C8202D00B47552 /* ReplaySubject.swift */,
FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */,
FD7115FB28C8155800B47552 /* Publisher+Utilities.swift */,
FD71160128C8255900B47552 /* UIControl+Combine.swift */,
FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */,
@ -4011,6 +4028,7 @@
isa = PBXGroup;
children = (
FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */,
FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */,
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */,
);
path = General;
@ -4020,9 +4038,14 @@
isa = PBXGroup;
children = (
FDC290A527D860CE005DAE71 /* Mock.swift */,
FD0969F82A69FFE700C5C365 /* Mocked.swift */,
FD23CE272A67755C0000B97C /* MockCrypto.swift */,
FD23CE2B2A678DF80000B97C /* MockCaches.swift */,
FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */,
FD23CE312A67C38D0000B97C /* MockNetwork.swift */,
FD96F3A629DBD43D00401309 /* MockJobRunner.swift */,
FD83B9BD27CF2243005E1583 /* TestConstants.swift */,
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */,
FDC290A727D9B46D005DAE71 /* NimbleExtensions.swift */,
FD078E4727E02561000769AF /* CommonMockedExtensions.swift */,
FD23EA6028ED0B260058676E /* CombineExtensions.swift */,
@ -4141,7 +4164,6 @@
FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */,
FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */,
FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */,
FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4155,8 +4177,6 @@
FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */,
FDC4381627B32EC700C60D73 /* Personalization.swift */,
FD2959912A4417A900888A17 /* PreparedSendData.swift */,
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */,
FDC438C227BB512200C60D73 /* SodiumProtocols.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4248,19 +4268,8 @@
isa = PBXGroup;
children = (
FDC438BC27BB2AB400C60D73 /* Mockable.swift */,
FD859EF327C2F49200510D0C /* MockSodium.swift */,
FD3C906E27E43E8700CD579F /* MockBox.swift */,
FD859EF927C2F5C500510D0C /* MockGenericHash.swift */,
FD859EF527C2F52C00510D0C /* MockSign.swift */,
FD859EF727C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift */,
FD859EFB27C2F60700510D0C /* MockEd25519.swift */,
FD078E5927E29F09000769AF /* MockNonce16Generator.swift */,
FD078E5B27E29F78000769AF /* MockNonce24Generator.swift */,
FD83B9D127D59495005E1583 /* MockUserDefaults.swift */,
FD078E4C27E17156000769AF /* MockOGMCache.swift */,
FDC290B227DFF9F5005DAE71 /* TestOnionRequestAPI.swift */,
FD078E4E27E175F1000769AF /* DependencyExtensions.swift */,
FD078E5127E1760A000769AF /* OGMDependencyExtensions.swift */,
);
path = _TestUtilities;
sourceTree = "<group>";
@ -4427,9 +4436,6 @@
C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */,
C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */,
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */,
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */,
C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */,
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */,
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */,
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
@ -4631,9 +4637,9 @@
D221A085169C9E5E00537ABF /* Sources */,
D221A086169C9E5E00537ABF /* Frameworks */,
D221A087169C9E5E00537ABF /* Resources */,
FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */,
453518771FC635DD00210559 /* Embed Foundation Extensions */,
4535189F1FC63DBF00210559 /* Embed Frameworks */,
FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */,
90DF4725BB1271EBA2C66A12 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
@ -5557,7 +5563,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FDF8488B29405BF2007DCAE5 /* SSKDependencies.swift in Sources */,
FDF8488E29405C04007DCAE5 /* GetSnodePoolJob.swift in Sources */,
FDF848C329405C5A007DCAE5 /* DeleteMessagesRequest.swift in Sources */,
FDF8489129405C13007DCAE5 /* SnodeAPINamespace.swift in Sources */,
@ -5629,10 +5634,13 @@
FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */,
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */,
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */,
FDF8488329405A12007DCAE5 /* BatchResponse.swift in Sources */,
C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */,
FDFF9FDF2A787F57005E0628 /* JSONEncoder+Utilities.swift in Sources */,
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */,
FD23CE1B2A651E6D0000B97C /* NetworkType.swift in Sources */,
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */,
FDA8EB10280F8238002B68E5 /* Codable+Utilities.swift in Sources */,
FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */,
@ -5690,8 +5698,10 @@
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */,
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
FD23CE1F2A65269C0000B97C /* Crypto.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FDF8488629405A61007DCAE5 /* Request.swift in Sources */,
FD23CE302A67B8820000B97C /* Caches.swift in Sources */,
FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */,
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */,
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
@ -5701,6 +5711,7 @@
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */,
B88FA7FB26114EA70049422F /* Hex.swift in Sources */,
FD7728962849E7E90018502F /* String+Utilities.swift in Sources */,
FD83DCDD2A739D350065FFAE /* RetryWithDependencies.swift in Sources */,
C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */,
C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */,
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */,
@ -5753,7 +5764,6 @@
FDF0B74928060D13004C14C5 /* QuotedReplyModel.swift in Sources */,
FD3003662A25D5B300B5A5FB /* ConfigMessageReceiveJob.swift in Sources */,
7B81682C28B72F480069F315 /* PendingChange.swift in Sources */,
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */,
FD5C7309285007920029977D /* BlindedIdLookup.swift in Sources */,
FD71161C28D194FB00B47552 /* MentionInfo.swift in Sources */,
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */,
@ -5851,9 +5861,9 @@
FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */,
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */,
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */,
FD23CE222A661D000000B97C /* OpenGroupAPI+Crypto.swift in Sources */,
FD245C652850665400B966DD /* ClosedGroupControlMessage.swift in Sources */,
FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */,
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */,
7B93D07027CF194000811CB6 /* ConfigurationMessage+Convenience.swift in Sources */,
FD5C72FD284F0EC90029977D /* MessageReceiver+ExpirationTimers.swift in Sources */,
C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */,
@ -5882,7 +5892,7 @@
FDA1E83B29A5F2D500C5C3BD /* SessionUtil+Shared.swift in Sources */,
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
FD16AB612A1DD9B60083D849 /* ProfilePictureView+Convenience.swift in Sources */,
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
FD23CE242A675C440000B97C /* Crypto+SessionMessagingKit.swift in Sources */,
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,
FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */,
@ -5914,7 +5924,6 @@
FD09798127FCFEE800936362 /* SessionThread.swift in Sources */,
FD09C5EA282A1BB2000CE219 /* ThreadTypingIndicator.swift in Sources */,
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */,
FDC438C127BB4E6800C60D73 /* SMKDependencies.swift in Sources */,
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */,
B806ECA126C4A7E4008BDA44 /* WebRTCSession+UI.swift in Sources */,
FD432432299C6933008A0213 /* _011_AddPendingReadReceipts.swift in Sources */,
@ -6140,14 +6149,18 @@
files = (
FD71161728D00DA400B47552 /* ThreadSettingsViewModelSpec.swift in Sources */,
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD23CE292A6775650000B97C /* MockCrypto.swift in Sources */,
FD23CE332A67C4D90000B97C /* MockNetwork.swift in Sources */,
FD71161528D00D6700B47552 /* ThreadDisappearingMessagesViewModelSpec.swift in Sources */,
FD23CE2D2A678E1E0000B97C /* MockCaches.swift in Sources */,
FD23EA5E28ED00FD0058676E /* NimbleExtensions.swift in Sources */,
FD23EA5F28ED00FF0058676E /* CommonMockedExtensions.swift in Sources */,
FD96F3A829DBD4AD00401309 /* MockJobRunner.swift in Sources */,
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */,
FD71161A28D00E1100B47552 /* NotificationContentViewModelSpec.swift in Sources */,
FD0969FA2A6A00B000C5C365 /* Mocked.swift in Sources */,
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */,
FD23EA6128ED0B260058676E /* CombineExtensions.swift in Sources */,
FD9DD2712A72516D00ECB68E /* TestExtensions.swift in Sources */,
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -6157,18 +6170,23 @@
buildActionMask = 2147483647;
files = (
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD96F3A929DBD4AD00401309 /* MockJobRunner.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FD9DD2732A72516D00ECB68E /* TestExtensions.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
FD23CE352A67C4DA0000B97C /* MockNetwork.swift in Sources */,
FD23CE282A67755C0000B97C /* MockCrypto.swift in Sources */,
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */,
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
FD23CE2C2A678DF80000B97C /* MockCaches.swift in Sources */,
FD0B77B229B82B7A009169BA /* ArrayUtilitiesSpec.swift in Sources */,
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
FD23CE262A676B5B0000B97C /* DependenciesSpec.swift in Sources */,
FDDF074A29DAB36900E5E8B5 /* JobRunnerSpec.swift in Sources */,
FD0969FB2A6A00B100C5C365 /* Mocked.swift in Sources */,
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
FD2959902A43BE5F00888A17 /* VersionSpec.swift in Sources */,
);
@ -6180,56 +6198,49 @@
files = (
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */,
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */,
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */,
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */,
FD3C906A27E417CE00CD579F /* CryptoSMKSpec.swift in Sources */,
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */,
FDBB25E12983909300F1508E /* ConfigConvoInfoVolatileSpec.swift in Sources */,
FD3C907127E445E500CD579F /* MessageReceiverDecryptionSpec.swift in Sources */,
FDC2909627D71252005DAE71 /* SOGSErrorSpec.swift in Sources */,
FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */,
FD078E4F27E175F1000769AF /* DependencyExtensions.swift in Sources */,
FDC2909C27D713D2005DAE71 /* SodiumProtocolsSpec.swift in Sources */,
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */,
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
FD23CE2A2A6775660000B97C /* MockCrypto.swift in Sources */,
FDC2909827D7129B005DAE71 /* PersonalizationSpec.swift in Sources */,
FD859EF427C2F49200510D0C /* MockSodium.swift in Sources */,
FD078E4D27E17156000769AF /* MockOGMCache.swift in Sources */,
FD078E5227E1760A000769AF /* OGMDependencyExtensions.swift in Sources */,
FD9DD2722A72516D00ECB68E /* TestExtensions.swift in Sources */,
FDA1E83629A5748F00C5C3BD /* ConfigUserGroupsSpec.swift in Sources */,
FD859EFC27C2F60700510D0C /* MockEd25519.swift in Sources */,
FDC290A627D860CE005DAE71 /* Mock.swift in Sources */,
FD2B4AFB29429D1000AB4848 /* ConfigContactsSpec.swift in Sources */,
FDA1E83D29AC71A800C5C3BD /* SessionUtilSpec.swift in Sources */,
FD83B9C027CF2294005E1583 /* TestConstants.swift in Sources */,
FD3C906F27E43E8700CD579F /* MockBox.swift in Sources */,
FDC4389A27BA002500C60D73 /* OpenGroupAPISpec.swift in Sources */,
FD23CE2E2A678E1E0000B97C /* MockCaches.swift in Sources */,
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FD23CE342A67C4D90000B97C /* MockNetwork.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD7692F72A53A2ED000E4B70 /* SessionThreadViewModelSpec.swift in Sources */,
FD0969F92A69FFE700C5C365 /* Mocked.swift in Sources */,
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FDA1E83929A5771A00C5C3BD /* LibSessionSpec.swift in Sources */,
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */,
FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */,
FDFD645D27F273F300808CA1 /* MockGeneralCache.swift in Sources */,
FD078E5A27E29F09000769AF /* MockNonce16Generator.swift in Sources */,
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */,
FDC2908D27D70905005DAE71 /* UpdateMessageRequestSpec.swift in Sources */,
FD078E5427E197CA000769AF /* OpenGroupManagerSpec.swift in Sources */,
FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */,
FD83B9D227D59495005E1583 /* MockUserDefaults.swift in Sources */,
FD078E5C27E29F78000769AF /* MockNonce24Generator.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -35,15 +35,15 @@ extension ContextMenuVC {
// MARK: - Actions
static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func info(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_info"),
title: "context_menu_info".localized(),
accessibilityLabel: "Message info"
) { delegate?.info(cellViewModel) }
) { delegate?.info(cellViewModel, using: dependencies) }
}
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func retry(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(systemName: "arrow.triangle.2.circlepath"),
title: (cellViewModel.state == .failedToSync ?
@ -51,23 +51,23 @@ extension ContextMenuVC {
"context_menu_resend".localized()
),
accessibilityLabel: (cellViewModel.state == .failedToSync ? "Resync message" : "Resend message")
) { delegate?.retry(cellViewModel) }
) { delegate?.retry(cellViewModel, using: dependencies) }
}
static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_reply"),
title: "context_menu_reply".localized(),
accessibilityLabel: "Reply to message"
) { delegate?.reply(cellViewModel) }
) { delegate?.reply(cellViewModel, using: dependencies) }
}
static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func copy(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_copy"),
title: "copy".localized(),
accessibilityLabel: "Copy text"
) { delegate?.copy(cellViewModel) }
) { delegate?.copy(cellViewModel, using: dependencies) }
}
static func copySessionID(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
@ -79,50 +79,50 @@ extension ContextMenuVC {
) { delegate?.copySessionID(cellViewModel) }
}
static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_trash"),
title: "TXT_DELETE_TITLE".localized(),
accessibilityLabel: "Delete message"
) { delegate?.delete(cellViewModel) }
) { delegate?.delete(cellViewModel, using: dependencies) }
}
static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_download"),
title: "context_menu_save".localized(),
accessibilityLabel: "Save attachment"
) { delegate?.save(cellViewModel) }
) { delegate?.save(cellViewModel, using: dependencies) }
}
static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func ban(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_block"),
title: "context_menu_ban_user".localized(),
accessibilityLabel: "Ban user"
) { delegate?.ban(cellViewModel) }
) { delegate?.ban(cellViewModel, using: dependencies) }
}
static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
icon: UIImage(named: "ic_block"),
title: "context_menu_ban_and_delete_all".localized(),
accessibilityLabel: "Ban user and delete"
) { delegate?.banAndDeleteAllMessages(cellViewModel) }
) { delegate?.banAndDeleteAllMessages(cellViewModel, using: dependencies) }
}
static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?) -> Action {
static func react(_ cellViewModel: MessageViewModel, _ emoji: EmojiWithSkinTones, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
title: emoji.rawValue,
isEmojiAction: true
) { delegate?.react(cellViewModel, with: emoji) }
) { delegate?.react(cellViewModel, with: emoji, using: dependencies) }
}
static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
static func emojiPlusButton(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?, using dependencies: Dependencies) -> Action {
return Action(
isEmojiPlus: true,
accessibilityLabel: "Add emoji"
) { delegate?.showFullEmojiKeyboard(cellViewModel) }
) { delegate?.showFullEmojiKeyboard(cellViewModel, using: dependencies) }
}
static func dismiss(_ delegate: ContextMenuActionDelegate?) -> Action {
@ -150,7 +150,8 @@ extension ContextMenuVC {
currentUserBlinded25PublicKey: String?,
currentUserIsOpenGroupModerator: Bool,
currentThreadIsMessageRequest: Bool,
delegate: ContextMenuActionDelegate?
delegate: ContextMenuActionDelegate?,
using dependencies: Dependencies = Dependencies()
) -> [Action]? {
switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall,
@ -159,7 +160,7 @@ extension ContextMenuVC {
.infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving,
.infoMessageRequestAccepted, .infoDisappearingMessagesUpdate:
// Let the user delete info messages and unsent messages
return [ Action.delete(cellViewModel, delegate) ]
return [ Action.delete(cellViewModel, delegate, using: dependencies) ]
case .standardOutgoing, .standardIncoming: break
}
@ -227,18 +228,21 @@ extension ContextMenuVC {
let shouldShowInfo: Bool = (cellViewModel.attachments?.isEmpty == false)
let generatedActions: [Action] = [
(canRetry ? Action.retry(cellViewModel, delegate) : nil),
(viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate) : nil),
(canCopy ? Action.copy(cellViewModel, delegate) : nil),
(canSave ? Action.save(cellViewModel, delegate) : nil),
(canRetry ? Action.retry(cellViewModel, delegate, using: dependencies) : nil),
(viewModelCanReply(cellViewModel) ? Action.reply(cellViewModel, delegate, using: dependencies) : nil),
(canCopy ? Action.copy(cellViewModel, delegate, using: dependencies) : nil),
(canSave ? Action.save(cellViewModel, delegate, using: dependencies) : nil),
(canCopySessionId ? Action.copySessionID(cellViewModel, delegate) : nil),
(canDelete ? Action.delete(cellViewModel, delegate) : nil),
(canBan ? Action.ban(cellViewModel, delegate) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate) : nil),
(canDelete ? Action.delete(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.ban(cellViewModel, delegate, using: dependencies) : nil),
(canBan ? Action.banAndDeleteAllMessages(cellViewModel, delegate, using: dependencies) : nil),
(shouldShowInfo ? Action.info(cellViewModel, delegate, using: dependencies) : nil),
]
.appending(contentsOf: (shouldShowEmojiActions ? recentEmojis : []).map { Action.react(cellViewModel, $0, delegate) })
.appending(Action.emojiPlusButton(cellViewModel, delegate))
.appending(
contentsOf: (shouldShowEmojiActions ? recentEmojis : [])
.map { Action.react(cellViewModel, $0, delegate, using: dependencies) }
)
.appending(Action.emojiPlusButton(cellViewModel, delegate, using: dependencies))
.compactMap { $0 }
guard !generatedActions.isEmpty else { return [] }
@ -250,16 +254,16 @@ extension ContextMenuVC {
// MARK: - Delegate
protocol ContextMenuActionDelegate {
func info(_ cellViewModel: MessageViewModel)
func retry(_ cellViewModel: MessageViewModel)
func reply(_ cellViewModel: MessageViewModel)
func copy(_ cellViewModel: MessageViewModel)
func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func copySessionID(_ cellViewModel: MessageViewModel)
func delete(_ cellViewModel: MessageViewModel)
func save(_ cellViewModel: MessageViewModel)
func ban(_ cellViewModel: MessageViewModel)
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones)
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel)
func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies)
func contextMenuDismissed()
}

View File

@ -173,8 +173,8 @@ extension ConversationVC:
// MARK: - AttachmentApprovalViewControllerDelegate
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendMessage(text: (messageText ?? ""), attachments: attachments)
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) {
sendMessage(text: (messageText ?? ""), attachments: attachments, using: dependencies)
resetMentions()
dismiss(animated: true) { [weak self] in
@ -409,7 +409,8 @@ extension ConversationVC:
attachments: [SignalAttachment] = [],
linkPreviewDraft: LinkPreviewDraft? = nil,
quoteModel: QuotedReplyModel? = nil,
hasPermissionToSendSeed: Bool = false
hasPermissionToSendSeed: Bool = false,
using dependencies: Dependencies = Dependencies()
) {
guard !showBlockedModalIfNeeded() else { return }
@ -488,7 +489,7 @@ extension ConversationVC:
let quoteThumbnailAttachment: Attachment? = quoteModel?.attachment?.cloneAsQuoteThumbnail()
// Actually send the message
Storage.shared
dependencies.storage
.writePublisher { [weak self] db in
// Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false {
@ -536,7 +537,8 @@ extension ConversationVC:
db,
interaction: insertedInteraction,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
@ -787,10 +789,14 @@ extension ConversationVC:
self.contextMenuWindow?.makeKeyAndVisible()
}
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) {
func handleItemTapped(
_ cellViewModel: MessageViewModel,
gestureRecognizer: UITapGestureRecognizer,
using dependencies: Dependencies = Dependencies()
) {
guard cellViewModel.variant != .standardOutgoing || (cellViewModel.state != .failed && cellViewModel.state != .failedToSync) else {
// Show the failed message sheet
showFailedMessageSheet(for: cellViewModel)
showFailedMessageSheet(for: cellViewModel, using: dependencies)
return
}
@ -864,8 +870,8 @@ extension ConversationVC:
let threadId: String = self.viewModel.threadData.threadId
// Retry downloading the failed attachment
Storage.shared.writeAsync { db in
JobRunner.add(
dependencies.storage.writeAsync { db in
dependencies.jobRunner.add(
db,
job: Job(
variant: .attachmentDownload,
@ -874,7 +880,9 @@ extension ConversationVC:
details: AttachmentDownloadJob.Details(
attachmentId: mediaView.attachment.id
)
)
),
canStartJob: true,
using: dependencies
)
}
break
@ -1013,8 +1021,8 @@ extension ConversationVC:
self.present(actionSheet, animated: true)
}
func handleReplyButtonTapped(for cellViewModel: MessageViewModel) {
reply(cellViewModel)
func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies) {
reply(cellViewModel, using: dependencies)
}
func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?) {
@ -1123,15 +1131,15 @@ extension ConversationVC:
UIView.setAnimationsEnabled(true)
}
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones) {
react(cellViewModel, with: emoji.rawValue, remove: false)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies) {
react(cellViewModel, with: emoji.rawValue, remove: false, using: dependencies)
}
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) {
react(cellViewModel, with: emoji.rawValue, remove: true)
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies) {
react(cellViewModel, with: emoji.rawValue, remove: true, using: dependencies)
}
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String) {
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return }
Storage.shared
@ -1208,7 +1216,7 @@ extension ConversationVC:
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
let openGroupRoom: String? = self.viewModel.threadData.openGroupRoomToken
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let recentReactionTimestamps: [Int64] = dependencies.generalCache.recentReactionTimestamps
let recentReactionTimestamps: [Int64] = dependencies.caches[.general].recentReactionTimestamps
guard
recentReactionTimestamps.count < 20 ||
@ -1226,7 +1234,7 @@ extension ConversationVC:
return
}
dependencies.mutableGeneralCache.mutate {
dependencies.caches.mutate(cache: .general) {
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps
.suffix(19))
.appending(sentTimestamp)
@ -1261,9 +1269,9 @@ extension ConversationVC:
))
}
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.flatMap { pendingChange -> AnyPublisher<(MessageSender.PreparedSendData?, OpenGroupInfo?), Error> in
Storage.shared.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in
dependencies.storage.writePublisher { [weak self] db -> (MessageSender.PreparedSendData?, OpenGroupInfo?) in
// Update the thread to be visible (if it isn't already)
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread
@ -1372,7 +1380,8 @@ extension ConversationVC:
namespace: try Message.Destination
.from(db, threadId: cellViewModel.threadId, threadVariant: cellViewModel.threadVariant)
.defaultNamespace,
interactionId: cellViewModel.id
interactionId: cellViewModel.id,
using: dependencies
)
return (sendData, nil)
@ -1382,7 +1391,7 @@ extension ConversationVC:
.tryFlatMap { messageSendData, openGroupInfo -> AnyPublisher<Void, Error> in
switch (messageSendData, openGroupInfo) {
case (.some(let sendData), _):
return MessageSender.sendImmediate(preparedSendData: sendData)
return MessageSender.sendImmediate(data: sendData, using: dependencies)
case (_, .some(let info)):
return OpenGroupAPI.send(data: info.sendData)
@ -1433,14 +1442,14 @@ extension ConversationVC:
}
}
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel) {
func showFullEmojiKeyboard(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
hideInputAccessoryView()
let emojiPicker = EmojiPickerSheet(
completionHandler: { [weak self] emoji in
guard let emoji: EmojiWithSkinTones = emoji else { return }
self?.react(cellViewModel, with: emoji)
self?.react(cellViewModel, with: emoji, using: dependencies)
},
dismissHandler: { [weak self] in
self?.showInputAccessoryView()
@ -1456,7 +1465,7 @@ extension ConversationVC:
// MARK: --action handling
func showFailedMessageSheet(for cellViewModel: MessageViewModel) {
private func showFailedMessageSheet(for cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let sheet = UIAlertController(
title: (cellViewModel.state == .failedToSync ?
"MESSAGE_DELIVERY_FAILED_SYNC_TITLE".localized() :
@ -1483,7 +1492,7 @@ extension ConversationVC:
"context_menu_resend".localized()
),
style: .default,
handler: { [weak self] _ in self?.retry(cellViewModel) }
handler: { [weak self] _ in self?.retry(cellViewModel, using: dependencies) }
))
// HACK: Extracting this info from the error string is pretty dodgy
@ -1596,7 +1605,7 @@ extension ConversationVC:
// MARK: - ContextMenuActionDelegate
func info(_ cellViewModel: MessageViewModel) {
func info(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let mediaInfoVC = MediaInfoVC(
attachments: (cellViewModel.attachments ?? []),
isOutgoing: (cellViewModel.variant == .standardOutgoing),
@ -1607,8 +1616,8 @@ extension ConversationVC:
navigationController?.pushViewController(mediaInfoVC, animated: true)
}
func retry(_ cellViewModel: MessageViewModel) {
Storage.shared.writeAsync { [weak self] db in
func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
dependencies.storage.writeAsync { [weak self] db in
guard
let threadId: String = self?.viewModel.threadData.threadId,
let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant,
@ -1649,12 +1658,13 @@ extension ConversationVC:
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant,
isSyncMessage: (cellViewModel.state == .failedToSync)
isSyncMessage: (cellViewModel.state == .failedToSync),
using: dependencies
)
}
}
func reply(_ cellViewModel: MessageViewModel) {
func reply(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
let maybeQuoteDraft: QuotedReplyModel? = QuotedReplyModel.quotedReplyForSending(
threadId: self.viewModel.threadData.threadId,
authorId: cellViewModel.authorId,
@ -1677,7 +1687,7 @@ extension ConversationVC:
snInputView.becomeFirstResponder()
}
func copy(_ cellViewModel: MessageViewModel) {
func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break
@ -1715,7 +1725,7 @@ extension ConversationVC:
UIPasteboard.general.string = cellViewModel.authorId
}
func delete(_ cellViewModel: MessageViewModel) {
func delete(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
switch cellViewModel.variant {
case .standardIncomingDeleted, .infoCall,
.infoScreenshotNotification, .infoMediaSavedNotification,
@ -1911,7 +1921,8 @@ extension ConversationVC:
message: unsendRequest,
threadId: cellViewModel.threadId,
interactionId: nil,
to: .contact(publicKey: userPublicKey)
to: .contact(publicKey: userPublicKey),
using: dependencies
)
}
return
@ -1934,7 +1945,8 @@ extension ConversationVC:
message: unsendRequest,
threadId: cellViewModel.threadId,
interactionId: nil,
to: .contact(publicKey: userPublicKey)
to: .contact(publicKey: userPublicKey),
using: dependencies
)
}
self?.showInputAccessoryView()
@ -1962,7 +1974,8 @@ extension ConversationVC:
message: unsendRequest,
interactionId: nil,
threadId: cellViewModel.threadId,
threadVariant: cellViewModel.threadVariant
threadVariant: cellViewModel.threadVariant,
using: dependencies
)
}
@ -1996,7 +2009,7 @@ extension ConversationVC:
}
}
func save(_ cellViewModel: MessageViewModel) {
func save(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.cellType == .mediaMessage else { return }
let mediaAttachments: [(Attachment, String)] = (cellViewModel.attachments ?? [])
@ -2038,24 +2051,10 @@ extension ConversationVC:
return
}
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Storage.shared.writeAsync { db in
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)),
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
)
}
sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs)))
}
func ban(_ cellViewModel: MessageViewModel) {
func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId
@ -2111,7 +2110,7 @@ extension ConversationVC:
self.present(modal, animated: true)
}
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel) {
func banAndDeleteAllMessages(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.threadVariant == .community else { return }
let threadId: String = self.viewModel.threadData.threadId
@ -2303,23 +2302,29 @@ extension ConversationVC:
// MARK: - Data Extraction Notifications
@objc func sendScreenshotNotification() {
@objc func sendScreenshotNotification() { sendDataExtraction(kind: .screenshot) }
func sendDataExtraction(
kind: DataExtractionNotification.Kind,
using dependencies: Dependencies = Dependencies()
) {
// Only send screenshot notifications to one-to-one conversations
guard self.viewModel.threadData.threadVariant == .contact else { return }
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
Storage.shared.writeAsync { db in
dependencies.storage.writeAsync { db in
try MessageSender.send(
db,
message: DataExtractionNotification(
kind: .screenshot,
kind: kind,
sentTimestamp: UInt64(SnodeAPI.currentOffsetTimestampMs())
),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
}
@ -2355,7 +2360,8 @@ extension ConversationVC {
for threadId: String,
threadVariant: SessionThread.Variant,
isNewThread: Bool,
timestampMs: Int64
timestampMs: Int64,
using dependencies: Dependencies = Dependencies()
) {
guard threadVariant == .contact else { return }
@ -2396,7 +2402,8 @@ extension ConversationVC {
),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}

View File

@ -87,12 +87,18 @@ public class MessageCell: UITableViewCell {
protocol MessageCellDelegate: ReactionDelegate {
func handleItemLongPressed(_ cellViewModel: MessageViewModel)
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer)
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer, using dependencies: Dependencies)
func handleItemDoubleTapped(_ cellViewModel: MessageViewModel)
func handleItemSwiped(_ cellViewModel: MessageViewModel, state: SwipeState)
func openUrl(_ urlString: String)
func handleReplyButtonTapped(for cellViewModel: MessageViewModel)
func handleReplyButtonTapped(for cellViewModel: MessageViewModel, using dependencies: Dependencies)
func startThread(with sessionId: String, openGroupServer: String?, openGroupPublicKey: String?)
func showReactionList(_ cellViewModel: MessageViewModel, selectedReaction: EmojiWithSkinTones?)
func needsLayout(for cellViewModel: MessageViewModel, expandingReactions: Bool)
}
extension MessageCellDelegate {
func handleItemTapped(_ cellViewModel: MessageViewModel, gestureRecognizer: UITapGestureRecognizer) {
handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer, using: Dependencies())
}
}

View File

@ -861,7 +861,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
isHandlingLongPress = true
}
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) { onTap(gestureRecognizer) }
private func onTap(_ gestureRecognizer: UITapGestureRecognizer, using dependencies: Dependencies = Dependencies()) {
guard let cellViewModel: MessageViewModel = self.viewModel else { return }
let location = gestureRecognizer.location(in: self)
@ -897,10 +899,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
if reactionContainerView.convert(reactionView.frame, from: reactionView.superview).contains(convertedLocation) {
if reactionView.viewModel.showBorder {
delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji)
delegate?.removeReact(cellViewModel, for: reactionView.viewModel.emoji, using: dependencies)
}
else {
delegate?.react(cellViewModel, with: reactionView.viewModel.emoji)
delegate?.react(cellViewModel, with: reactionView.viewModel.emoji, using: dependencies)
}
return
}
@ -917,7 +919,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}
}
else if snContentView.bounds.contains(snContentView.convert(location, from: self)) {
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer)
delegate?.handleItemTapped(cellViewModel, gestureRecognizer: gestureRecognizer, using: dependencies)
}
}
@ -985,11 +987,11 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
}
}
private func reply() {
private func reply(using dependencies: Dependencies = Dependencies()) {
guard let cellViewModel: MessageViewModel = self.viewModel else { return }
resetReply()
delegate?.handleReplyButtonTapped(for: cellViewModel)
delegate?.handleReplyButtonTapped(for: cellViewModel, using: dependencies)
}
// MARK: - Convenience

View File

@ -39,10 +39,10 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
// MARK: - Initialization
init(
dependencies: Dependencies = Dependencies(),
threadId: String,
threadVariant: SessionThread.Variant,
config: DisappearingMessagesConfiguration
config: DisappearingMessagesConfiguration,
using dependencies: Dependencies = Dependencies()
) {
self.dependencies = dependencies
self.threadId = threadId
@ -68,7 +68,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
currentSelection
.removeDuplicates()
.map { [weak self] currentSelection in (self?.storedSelection != currentSelection) }
.map { isChanged in
.map { [weak self, dependencies] isChanged in
guard isChanged else { return [] }
return [
@ -76,8 +76,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
id: .save,
systemItem: .save,
accessibilityIdentifier: "Save button"
) { [weak self] in
self?.saveChanges()
) {
self?.saveChanges(using: dependencies)
self?.dismissScreen()
}
]
@ -100,7 +100,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, config, dependencies, threadId = self.threadId] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db)
@ -156,7 +156,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
// MARK: - Functions
private func saveChanges() {
private func saveChanges(using dependencies: Dependencies = Dependencies()) {
let threadId: String = self.threadId
let threadVariant: SessionThread.Variant = self.threadVariant
let currentSelection: TimeInterval = self.currentSelection.value
@ -195,7 +195,8 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
),
interactionId: interaction.id,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
// Legacy closed groups

View File

@ -60,10 +60,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
// MARK: - Initialization
init(
dependencies: Dependencies = Dependencies(),
threadId: String,
threadVariant: SessionThread.Variant,
didTriggerSearch: @escaping () -> ()
didTriggerSearch: @escaping () -> (),
using dependencies: Dependencies = Dependencies()
) {
self.dependencies = dependencies
self.threadId = threadId
@ -196,7 +196,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
/// just in case the database has changed between the two reads - unfortunately it doesn't look like there is a way to prevent this
private lazy var _observableTableData: ObservableData = ValueObservation
.trackingConstantRegion { [weak self, dependencies, threadId = self.threadId, threadVariant = self.threadVariant] db -> [SectionModel] in
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let maybeThreadViewModel: SessionThreadViewModel? = try SessionThreadViewModel
.conversationSettingsQuery(threadId: threadId, userPublicKey: userPublicKey)
.fetchOne(db)
@ -755,7 +755,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
publicKey: publicKey
)
dependencies.storage.writeAsync { db in
dependencies.storage.writeAsync { [dependencies] db in
try selectedUsers.forEach { userId in
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: userId, variant: .contact, shouldBeVisible: nil)
@ -786,7 +786,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
db,
interaction: interaction,
threadId: thread.id,
threadVariant: thread.variant
threadVariant: thread.variant,
using: dependencies
)
}
}

View File

@ -368,10 +368,12 @@ final class ReactionListSheet: BaseVC {
dismiss(animated: true, completion: nil)
}
@objc private func clearAllTapped() {
@objc private func clearAllTapped() { clearAll() }
private func clearAll(using dependencies: Dependencies = Dependencies()) {
guard let selectedReaction: EmojiWithSkinTones = self.reactionSummaries.first(where: { $0.isSelected })?.emoji else { return }
delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue)
delegate?.removeAllReactions(messageViewModel, for: selectedReaction.rawValue, using: dependencies)
}
}
@ -599,7 +601,13 @@ extension ReactionListSheet {
// MARK: - Delegate
protocol ReactionDelegate: AnyObject {
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones)
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones)
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String)
func react(_ cellViewModel: MessageViewModel, with emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones, using dependencies: Dependencies)
func removeAllReactions(_ cellViewModel: MessageViewModel, for emoji: String, using dependencies: Dependencies)
}
extension ReactionDelegate {
func removeReact(_ cellViewModel: MessageViewModel, for emoji: EmojiWithSkinTones) {
removeReact(cellViewModel, for: emoji, using: Dependencies())
}
}

View File

@ -505,8 +505,9 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
dismissSelf(animated: true)
}
@objc
public func didPressShare(_ sender: Any) {
@objc public func didPressShare(_ sender: Any) { share() }
public func share(using dependencies: Dependencies = Dependencies()) {
guard let currentViewController = self.viewControllers?[0] as? MediaDetailViewController else {
owsFailDebug("currentViewController was unexpectedly nil")
return
@ -553,7 +554,8 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
),
interactionId: nil, // Show no interaction for the current user
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
}

View File

@ -418,7 +418,7 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
attachmentDraftCollection.remove(attachment: attachment)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText)
}

View File

@ -553,7 +553,8 @@ class NotificationActionHandler {
func reply(
userInfo: [AnyHashable: Any],
replyText: String,
applicationState: UIApplication.State
applicationState: UIApplication.State,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else {
return Fail<Void, Error>(error: NotificationError.failDebug("threadId was unexpectedly nil"))
@ -599,10 +600,11 @@ class NotificationActionHandler {
db,
interaction: interaction,
threadId: threadId,
threadVariant: thread.variant
threadVariant: thread.variant,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.handleEvents(
receiveCompletion: { result in
switch result {

View File

@ -20,7 +20,7 @@ public enum SyncPushTokensJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) {
// Don't run when inactive or not in main app or if the user doesn't exist yet
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {

View File

@ -26,7 +26,10 @@ enum Onboarding {
return existingPublisher
}
private static func createProfileNameRetrievalPublisher(_ requestId: UUID) -> AnyPublisher<String?, Error> {
private static func createProfileNameRetrievalPublisher(
_ requestId: UUID,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<String?, Error> {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard SessionUtil.userConfigsEnabled else {
return Just(nil)
@ -99,7 +102,8 @@ enum Onboarding {
)
}(),
sentTimestamp: TimeInterval((message.sentTimestamp ?? 0) / 1000),
calledFromConfigHandling: false
calledFromConfigHandling: false,
using: dependencies
)
}
return ()

View File

@ -244,7 +244,7 @@ final class NukeDataModal: Modal {
UserDefaults.removeAll()
// Remove the cached key so it gets re-cached on next access
dependencies.mutableGeneralCache.mutate {
dependencies.caches.mutate(cache: .general) {
$0.encodedPublicKey = nil
$0.recentReactionTimestamps = []
}

View File

@ -13,10 +13,7 @@ public final class BackgroundPoller {
public static func poll(
completionHandler: @escaping (UIBackgroundFetchResult) -> Void,
dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies(
subscribeQueue: .global(qos: .background),
receiveQueue: .main
)
using dependencies: Dependencies = Dependencies()
) {
Publishers
.MergeMany(
@ -55,8 +52,8 @@ public final class BackgroundPoller {
}
)
)
.subscribe(on: dependencies.subscribeQueue)
.receive(on: dependencies.receiveQueue)
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.receive(on: DispatchQueue.main, using: dependencies)
.collect()
.sinkUntilComplete(
receiveCompletion: { result in
@ -74,7 +71,7 @@ public final class BackgroundPoller {
}
private static func pollForMessages(
using dependencies: OpenGroupManager.OGMDependencies
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
let userPublicKey: String = getUserHexEncodedPublicKey()
@ -94,7 +91,7 @@ public final class BackgroundPoller {
}
private static func pollForClosedGroupMessages(
using dependencies: OpenGroupManager.OGMDependencies
using dependencies: Dependencies
) -> [AnyPublisher<Void, Error>] {
// Fetch all closed groups (excluding any don't contain the current user as a
// GroupMemeber as the user is no longer a member of those)

View File

@ -124,13 +124,14 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
_ db: Database,
message: CallMessage,
interactionId: Int64?,
in thread: SessionThread
in thread: SessionThread,
using dependencies: Dependencies = Dependencies()
) throws -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending pre-offer message.")
return MessageSender
.sendImmediate(
preparedSendData: try MessageSender
data: try MessageSender
.preparedSendData(
db,
message: message,
@ -138,8 +139,10 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: interactionId
)
interactionId: interactionId,
using: dependencies
),
using: dependencies
)
.handleEvents(receiveOutput: { _ in SNLog("[Calls] Pre-offer message has been sent.") })
.eraseToAnyPublisher()
@ -147,7 +150,8 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
public func sendOffer(
to thread: SessionThread,
isRestartingICEConnection: Bool = false
isRestartingICEConnection: Bool = false,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending offer message.")
let uuid: String = self.uuid
@ -172,7 +176,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}
}
Storage.shared
dependencies.storage
.writePublisher { db in
try MessageSender
.preparedSendData(
@ -188,10 +192,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
@ -207,12 +212,15 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
.eraseToAnyPublisher()
}
public func sendAnswer(to sessionId: String) -> AnyPublisher<Void, Error> {
public func sendAnswer(
to sessionId: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending answer message.")
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
return Storage.shared
return dependencies.storage
.readPublisher { db -> SessionThread in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
throw WebRTCSessionError.noThread
@ -239,7 +247,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}
}
Storage.shared
dependencies.storage
.writePublisher { db in
try MessageSender
.preparedSendData(
@ -254,10 +262,11 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete(
receiveCompletion: { result in
@ -283,7 +292,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
}
}
private func sendICECandidates() {
private func sendICECandidates(using dependencies: Dependencies = Dependencies()) {
let candidates: [RTCIceCandidate] = self.queuedICECandidates
let uuid: String = self.uuid
let contactSessionId: String = self.contactSessionId
@ -291,7 +300,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
// Empty the queue
self.queuedICECandidates.removeAll()
Storage.shared
dependencies.storage
.writePublisher { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
throw WebRTCSessionError.noThread
@ -315,15 +324,20 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.sinkUntilComplete()
}
public func endCall(_ db: Database, with sessionId: String) throws {
public func endCall(
_ db: Database,
with sessionId: String,
using dependencies: Dependencies = Dependencies()
) throws {
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: sessionId) else { return }
SNLog("[Calls] Sending end call message.")
@ -340,11 +354,12 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
MessageSender
.sendImmediate(preparedSendData: preparedSendData)
.sendImmediate(data: preparedSendData, using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
}

View File

@ -1039,7 +1039,10 @@ extension Attachment {
}
}
internal func upload(to destination: Attachment.Destination) -> AnyPublisher<String?, Error> {
internal func upload(
to destination: Attachment.Destination,
using dependencies: Dependencies
) -> AnyPublisher<String?, Error> {
// This can occur if an AttachmnetUploadJob was explicitly created for a message
// dependant on the attachment being uploaded (in this case the attachment has
// already been uploaded so just succeed)

View File

@ -76,7 +76,7 @@ public extension BlindedIdLookup {
openGroupServer: String,
openGroupPublicKey: String,
isCheckingForOutbox: Bool,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> BlindedIdLookup {
var lookup: BlindedIdLookup = (try? BlindedIdLookup
.fetchOne(db, id: blindedId))
@ -94,11 +94,13 @@ public extension BlindedIdLookup {
// If we we given a sessionId then validate it is correct and if so save it
if
let sessionId: String = sessionId,
dependencies.sodium.sessionId(
sessionId,
matchesBlindedId: blindedId,
serverPublicKey: openGroupPublicKey,
genericHash: dependencies.genericHash
dependencies.crypto.verify(
.sessionId(
sessionId,
matchesBlindedId: blindedId,
serverPublicKey: openGroupPublicKey,
using: dependencies
)
)
{
lookup = try lookup
@ -115,9 +117,16 @@ public extension BlindedIdLookup {
.fetchCursor(db)
while let contact: Contact = try contactsThatApprovedMeCursor.next() {
guard dependencies.sodium.sessionId(contact.id, matchesBlindedId: blindedId, serverPublicKey: openGroupPublicKey, genericHash: dependencies.genericHash) else {
continue
}
guard
dependencies.crypto.verify(
.sessionId(
contact.id,
matchesBlindedId: blindedId,
serverPublicKey: openGroupPublicKey,
using: dependencies
)
)
else { continue }
// We found a match so update the lookup and leave the loop
lookup = try lookup
@ -151,11 +160,13 @@ public extension BlindedIdLookup {
while let otherLookup: BlindedIdLookup = try blindedIdLookupCursor.next() {
guard
let sessionId: String = otherLookup.sessionId,
dependencies.sodium.sessionId(
sessionId,
matchesBlindedId: blindedId,
serverPublicKey: openGroupPublicKey,
genericHash: dependencies.genericHash
dependencies.crypto.verify(
.sessionId(
sessionId,
matchesBlindedId: blindedId,
serverPublicKey: openGroupPublicKey,
using: dependencies
)
)
else { continue }

View File

@ -55,12 +55,12 @@ public struct Contact: Codable, Identifiable, Equatable, FetchableRecord, Persis
isBlocked: Bool = false,
didApproveMe: Bool = false,
hasBeenBlocked: Bool = false,
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) {
self.id = id
self.isTrusted = (
isTrusted ||
id == getUserHexEncodedPublicKey(dependencies: dependencies) // Always trust ourselves
id == getUserHexEncodedPublicKey(using: dependencies) // Always trust ourselves
)
self.isApproved = isApproved
self.isBlocked = isBlocked

View File

@ -797,22 +797,19 @@ public extension Interaction {
_ db: Database,
threadId: String,
body: String?,
quoteAuthorId: String? = nil
quoteAuthorId: String? = nil,
using dependencies: Dependencies = Dependencies()
) -> Bool {
var publicKeysToCheck: [String] = [
getUserHexEncodedPublicKey(db)
getUserHexEncodedPublicKey(db, using: dependencies)
]
// If the thread is an open group then add the blinded id as a key to check
if let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: threadId) {
let sodium: Sodium = Sodium()
if
let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db),
let blindedKeyPair: KeyPair = sodium.blindedKeyPair(
serverPublicKey: openGroup.publicKey,
edKeyPair: userEd25519KeyPair,
genericHash: sodium.genericHash
let blindedKeyPair: KeyPair = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: openGroup.publicKey, edKeyPair: userEd25519KeyPair, using: dependencies)
)
{
publicKeysToCheck.append(SessionId(.blinded15, publicKey: blindedKeyPair.publicKey).hexString)

View File

@ -250,7 +250,7 @@ public extension Profile {
///
/// **Note:** This method intentionally does **not** save the newly created Profile,
/// it will need to be explicitly saved after calling
static func fetchOrCreateCurrentUser(_ db: Database? = nil, dependencies: Dependencies = Dependencies()) -> Profile {
static func fetchOrCreateCurrentUser(_ db: Database? = nil, using dependencies: Dependencies = Dependencies()) -> Profile {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
guard let db: Database = db else {

View File

@ -525,12 +525,19 @@ public extension SessionThread {
_ db: Database? = nil,
threadId: String,
threadVariant: Variant,
blindingPrefix: SessionId.Prefix
blindingPrefix: SessionId.Prefix,
using dependencies: Dependencies = Dependencies()
) -> String? {
guard threadVariant == .community else { return nil }
guard let db: Database = db else {
return Storage.shared.read { db in
getUserHexEncodedBlindedKey(db, threadId: threadId, threadVariant: threadVariant, blindingPrefix: blindingPrefix)
return dependencies.storage.read { db in
getUserHexEncodedBlindedKey(
db,
threadId: threadId,
threadVariant: threadVariant,
blindingPrefix: blindingPrefix,
using: dependencies
)
}
}
@ -559,12 +566,8 @@ public extension SessionThread {
guard capabilities.isEmpty || capabilities.contains(.blind) else { return nil }
let sodium: Sodium = Sodium()
let blindedKeyPair: KeyPair? = sodium.blindedKeyPair(
serverPublicKey: openGroupInfo.publicKey,
edKeyPair: userEdKeyPair,
genericHash: sodium.getGenericHash()
let blindedKeyPair: KeyPair? = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: openGroupInfo.publicKey, edKeyPair: userEdKeyPair, using: dependencies)
)
return blindedKeyPair.map { keyPair -> String in

View File

@ -17,7 +17,7 @@ public enum AttachmentDownloadJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,

View File

@ -17,7 +17,7 @@ public enum AttachmentUploadJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
@ -72,7 +72,7 @@ public enum AttachmentUploadJob: JobExecutor {
// reentrancy issues when the success/failure closures get called before the upload as the JobRunner
// will attempt to update the state of the job immediately
attachment
.upload(to: (openGroup.map { .openGroup($0) } ?? .fileServer))
.upload(to: (openGroup.map { .openGroup($0) } ?? .fileServer), using: dependencies)
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(
@ -95,7 +95,8 @@ public enum AttachmentUploadJob: JobExecutor {
message: details.message,
with: .other(error),
interactionId: interactionId,
isSyncMessage: details.isSyncMessage
isSyncMessage: details.isSyncMessage,
using: dependencies
)
}

View File

@ -15,7 +15,7 @@ public enum ConfigMessageReceiveJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) {
/// When the `configMessageReceive` job fails we want to unblock any `messageReceive` jobs it was blocking
/// to ensure the user isn't losing any messages - this generally _shouldn't_ happen but if it does then having a temporary

View File

@ -18,7 +18,7 @@ public enum ConfigurationSyncJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
SessionUtil.userConfigsEnabled,
@ -43,7 +43,7 @@ public enum ConfigurationSyncJob: JobExecutor {
// it again immediately which is pointless)
let updatedJob: Job? = dependencies.storage.write { db in
try job
.with(nextRunTimestamp: Date().timeIntervalSince1970 + maxRunFrequency)
.with(nextRunTimestamp: dependencies.dateNow.timeIntervalSince1970 + maxRunFrequency)
.saved(db)
}
@ -79,7 +79,7 @@ public enum ConfigurationSyncJob: JobExecutor {
.map { $0.obsoleteHashes }
.reduce([], +)
.asSet()
let jobStartTimestamp: TimeInterval = Date().timeIntervalSince1970
let jobStartTimestamp: TimeInterval = dependencies.dateNow.timeIntervalSince1970
SNLog("[ConfigurationSyncJob] For \(publicKey) started with \(pendingConfigChanges.count) change\(pendingConfigChanges.count == 1 ? "" : "s")")
dependencies.storage
@ -105,7 +105,8 @@ public enum ConfigurationSyncJob: JobExecutor {
return (snodeMessage, namespace)
},
allObsoleteHashes: Array(allObsoleteHashes)
allObsoleteHashes: Array(allObsoleteHashes),
using: dependencies
)
}
.subscribe(on: queue)
@ -223,7 +224,7 @@ public extension ConfigurationSyncJob {
)
),
canStartJob: true,
dependencies: dependencies
using: dependencies
)
return
}
@ -231,16 +232,16 @@ public extension ConfigurationSyncJob {
// Upsert a config sync job if needed
dependencies.jobRunner.upsert(
db,
job: ConfigurationSyncJob.createIfNeeded(db, publicKey: publicKey, dependencies: dependencies),
job: ConfigurationSyncJob.createIfNeeded(db, publicKey: publicKey, using: dependencies),
canStartJob: true,
dependencies: dependencies
using: dependencies
)
}
@discardableResult static func createIfNeeded(
_ db: Database,
publicKey: String,
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) -> Job? {
/// The ConfigurationSyncJob will automatically reschedule itself to run again after 3 seconds so if there is an existing
/// job then there is no need to create another instance
@ -266,7 +267,7 @@ public extension ConfigurationSyncJob {
)
}
static func run() -> AnyPublisher<Void, Error> {
static func run(using dependencies: Dependencies = Dependencies()) -> AnyPublisher<Void, Error> {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard SessionUtil.userConfigsEnabled else {
return Storage.shared
@ -276,17 +277,18 @@ public extension ConfigurationSyncJob {
// fresh install due to the migrations getting run)
guard Identity.userCompletedRequiredOnboarding(db) else { throw StorageError.generic }
let publicKey: String = getUserHexEncodedPublicKey(db)
let publicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
return try MessageSender.preparedSendData(
db,
message: try ConfigurationMessage.getCurrent(db),
to: Message.Destination.contact(publicKey: publicKey),
namespace: .default,
interactionId: nil
interactionId: nil,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.eraseToAnyPublisher()
}
@ -298,7 +300,8 @@ public extension ConfigurationSyncJob {
queue: .global(qos: .userInitiated),
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, error, _, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _, _ in }
deferred: { _, _ in },
using: dependencies
)
}
}

View File

@ -16,7 +16,7 @@ public enum DisappearingMessagesJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
// The 'backgroundTask' gets captured and cleared within the 'completion' block
let timestampNowMs: TimeInterval = TimeInterval(SnodeAPI.currentOffsetTimestampMs())

View File

@ -16,7 +16,7 @@ public enum FailedAttachmentDownloadsJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
var changeCount: Int = -1

View File

@ -15,7 +15,7 @@ public enum FailedMessageSendsJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
var changeCount: Int = -1
var attachmentChangeCount: Int = -1

View File

@ -23,7 +23,7 @@ public enum GarbageCollectionJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
/// Determine what types of data we want to collect (if we didn't provide any then assume we want to collect everything)
///
@ -33,18 +33,18 @@ public enum GarbageCollectionJob: JobExecutor {
.map { try? JSONDecoder().decode(Details.self, from: $0) }?
.typesToCollect)
.defaulting(to: Types.allCases)
let timestampNow: TimeInterval = Date().timeIntervalSince1970
let timestampNow: TimeInterval = dependencies.dateNow.timeIntervalSince1970
/// Only do a full collection if the job isn't the recurring one or it's been 23 hours since it last ran (23 hours so a user who opens the
/// app at about the same time every day will trigger the garbage collection) - since this runs when the app becomes active we
/// want to prevent it running to frequently (the app becomes active if a system alert, the notification center or the control panel
/// are shown)
let lastGarbageCollection: Date = UserDefaults.standard[.lastGarbageCollection]
let lastGarbageCollection: Date = dependencies.standardUserDefaults[.lastGarbageCollection]
.defaulting(to: Date.distantPast)
let finalTypesToCollect: Set<Types> = {
guard
job.behaviour != .recurringOnActive ||
Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60)
dependencies.dateNow.timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60)
else {
// Note: This should only contain the `Types` which are unlikely to ever cause
// a startup delay (ie. avoid mass deletions and file management)
@ -450,8 +450,8 @@ public enum GarbageCollectionJob: JobExecutor {
// If we did a full collection then update the 'lastGarbageCollection' date to
// prevent a full collection from running again in the next 23 hours
if job.behaviour == .recurringOnActive && Date().timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60) {
UserDefaults.standard[.lastGarbageCollection] = Date()
if job.behaviour == .recurringOnActive && dependencies.dateNow.timeIntervalSince(lastGarbageCollection) > (23 * 60 * 60) {
dependencies.standardUserDefaults[.lastGarbageCollection] = dependencies.dateNow
}
success(job, false, dependencies)

View File

@ -18,7 +18,7 @@ public enum GroupLeavingJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) {
guard
let detailsData: Data = job.details,
@ -51,10 +51,11 @@ public enum GroupLeavingJob: JobExecutor {
to: destination,
namespace: destination.defaultNamespace,
interactionId: job.interactionId,
isSyncMessage: false
isSyncMessage: false,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(

View File

@ -15,7 +15,7 @@ public enum MessageReceiveJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) {
guard
let threadId: String = job.threadId,

View File

@ -18,7 +18,7 @@ public enum MessageSendJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
let detailsData: Data = job.details,
@ -90,6 +90,10 @@ public enum MessageSendJob: JobExecutor {
switch attachment.state {
case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded:
return true
// If we've somehow got an attachment that is in an 'uploaded' state but doesn't
// have a 'downloadUrl' then it's invalid and needs to be re-uploaded
case .uploaded: return (attachment.downloadUrl == nil)
default: return false
}
@ -122,7 +126,7 @@ public enum MessageSendJob: JobExecutor {
)
}
.compactMap { attachmentId -> (jobId: Int64, job: Job)? in
JobRunner
dependencies.jobRunner
.insert(
db,
job: Job(
@ -171,13 +175,14 @@ public enum MessageSendJob: JobExecutor {
to: details.destination,
namespace: details.destination.defaultNamespace,
interactionId: job.interactionId,
isSyncMessage: details.isSyncMessage
isSyncMessage: details.isSyncMessage,
using: dependencies
)
}
.map { sendData in sendData.with(fileIds: messageFileIds) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.subscribe(on: queue)
.receive(on: queue)
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: queue, using: dependencies)
.receive(on: queue, using: dependencies)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {

View File

@ -16,7 +16,7 @@ public enum NotifyPushServerJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
let detailsData: Data = job.details,

View File

@ -15,7 +15,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
// Don't run when inactive or not in main app
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {

View File

@ -17,7 +17,7 @@ public enum SendReadReceiptsJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
@ -47,7 +47,7 @@ public enum SendReadReceiptsJob: JobExecutor {
isSyncMessage: false
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(
@ -59,7 +59,7 @@ public enum SendReadReceiptsJob: JobExecutor {
// another one for the same thread but with a 'nextRunTimestamp' set to the
// 'maxRunFrequency' value to throttle the read receipt requests
var shouldFinishCurrentJob: Bool = false
let nextRunTimestamp: TimeInterval = (Date().timeIntervalSince1970 + maxRunFrequency)
let nextRunTimestamp: TimeInterval = (dependencies.dateNow.timeIntervalSince1970 + maxRunFrequency)
let updatedJob: Job? = Storage.shared.write { db in
// If another 'sendReadReceipts' job was scheduled then update that one

View File

@ -15,7 +15,7 @@ public enum UpdateProfilePictureJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
// Don't run when inactive or not in main app
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
@ -24,8 +24,8 @@ public enum UpdateProfilePictureJob: JobExecutor {
// Only re-upload the profile picture if enough time has passed since the last upload
guard
let lastProfilePictureUpload: Date = UserDefaults.standard[.lastProfilePictureUpload],
Date().timeIntervalSince(lastProfilePictureUpload) > (14 * 24 * 60 * 60)
let lastProfilePictureUpload: Date = dependencies.standardUserDefaults[.lastProfilePictureUpload],
dependencies.dateNow.timeIntervalSince(lastProfilePictureUpload) > (14 * 24 * 60 * 60)
else {
// Reset the `nextRunTimestamp` value just in case the last run failed so we don't get stuck
// in a loop endlessly deferring the job
@ -42,7 +42,7 @@ public enum UpdateProfilePictureJob: JobExecutor {
}
// Note: The user defaults flag is updated in ProfileManager
let profile: Profile = Profile.fetchOrCreateCurrentUser(dependencies: dependencies)
let profile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies)
let profilePictureData: Data? = profile.profilePictureFileName
.map { ProfileManager.loadProfileData(with: $0) }

View File

@ -230,7 +230,8 @@ public extension Message {
static func processRawReceivedMessage(
_ db: Database,
rawMessage: SnodeReceivedMessage
rawMessage: SnodeReceivedMessage,
using dependencies: Dependencies = Dependencies()
) throws -> ProcessedMessage? {
guard let envelope = SNProtoEnvelope.from(rawMessage) else {
throw MessageReceiverError.invalidMessage
@ -242,7 +243,8 @@ public extension Message {
envelope: envelope,
serverExpirationTimestamp: (TimeInterval(rawMessage.info.expirationDateMs) / 1000),
serverHash: rawMessage.info.hash,
handleClosedGroupKeyUpdateMessages: true
handleClosedGroupKeyUpdateMessages: true,
using: dependencies
)
// Ensure we actually want to de-dupe messages for this namespace, otherwise just
@ -289,7 +291,8 @@ public extension Message {
static func processRawReceivedMessage(
_ db: Database,
serializedData: Data,
serverHash: String?
serverHash: String?,
using dependencies: Dependencies = Dependencies()
) throws -> ProcessedMessage? {
guard let envelope = try? SNProtoEnvelope.parseData(serializedData) else {
throw MessageReceiverError.invalidMessage
@ -303,7 +306,8 @@ public extension Message {
ControlMessageProcessRecord.defaultExpirationSeconds
),
serverHash: serverHash,
handleClosedGroupKeyUpdateMessages: true
handleClosedGroupKeyUpdateMessages: true,
using: dependencies
)
}
@ -312,7 +316,8 @@ public extension Message {
/// closed group key update messages (the `NotificationServiceExtension` does this itself)
static func processRawReceivedMessageAsNotification(
_ db: Database,
envelope: SNProtoEnvelope
envelope: SNProtoEnvelope,
using dependencies: Dependencies = Dependencies()
) throws -> ProcessedMessage? {
let processedMessage: ProcessedMessage? = try processRawReceivedMessage(
db,
@ -322,7 +327,8 @@ public extension Message {
ControlMessageProcessRecord.defaultExpirationSeconds
),
serverHash: nil,
handleClosedGroupKeyUpdateMessages: false
handleClosedGroupKeyUpdateMessages: false,
using: dependencies
)
return processedMessage
@ -334,7 +340,7 @@ public extension Message {
openGroupServerPublicKey: String,
message: OpenGroupAPI.Message,
data: Data,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> ProcessedMessage? {
// Need a sender in order to process the message
guard let sender: String = message.sender, let timestamp = message.posted else { return nil }
@ -357,7 +363,7 @@ public extension Message {
openGroupMessageServerId: message.id,
openGroupServerPublicKey: openGroupServerPublicKey,
handleClosedGroupKeyUpdateMessages: false,
dependencies: dependencies
using: dependencies
)
}
@ -368,7 +374,7 @@ public extension Message {
data: Data,
isOutgoing: Bool? = nil,
otherBlindedPublicKey: String? = nil,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> ProcessedMessage? {
// Note: The `posted` value is in seconds but all messages in the database use milliseconds for timestamps
let envelopeBuilder = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: UInt64(floor(message.posted * 1000)))
@ -390,7 +396,7 @@ public extension Message {
isOutgoing: isOutgoing,
otherBlindedPublicKey: otherBlindedPublicKey,
handleClosedGroupKeyUpdateMessages: false,
dependencies: dependencies
using: dependencies
)
}
@ -399,24 +405,26 @@ public extension Message {
openGroupId: String,
message: OpenGroupAPI.Message,
associatedPendingChanges: [OpenGroupAPI.PendingChange],
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) -> [Reaction] {
var results: [Reaction] = []
guard let reactions = message.reactions else { return results }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let blinded15UserPublicKey: String? = SessionThread
.getUserHexEncodedBlindedKey(
db,
threadId: openGroupId,
threadVariant: .community,
blindingPrefix: .blinded15
blindingPrefix: .blinded15,
using: dependencies
)
let blinded25UserPublicKey: String? = SessionThread
.getUserHexEncodedBlindedKey(
db,
threadId: openGroupId,
threadVariant: .community,
blindingPrefix: .blinded25
blindingPrefix: .blinded25,
using: dependencies
)
for (encodedEmoji, rawReaction) in reactions {
@ -536,7 +544,7 @@ public extension Message {
isOutgoing: Bool? = nil,
otherBlindedPublicKey: String? = nil,
handleClosedGroupKeyUpdateMessages: Bool,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> ProcessedMessage? {
let (message, proto, threadId, threadVariant) = try MessageReceiver.parse(
db,
@ -547,7 +555,7 @@ public extension Message {
openGroupServerPublicKey: openGroupServerPublicKey,
isOutgoing: isOutgoing,
otherBlindedPublicKey: otherBlindedPublicKey,
dependencies: dependencies
using: dependencies
)
message.serverHash = serverHash
@ -568,7 +576,8 @@ public extension Message {
db,
threadId: threadId,
threadVariant: threadVariant,
message: closedGroupControlMessage
message: closedGroupControlMessage,
using: dependencies
)
return nil

View File

@ -0,0 +1,403 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoKit
import Sodium
import Clibsodium
import Curve25519Kit
import SessionUtilitiesKit
// MARK: - Nonce
internal extension OpenGroupAPI {
class NonceGenerator16Byte: NonceGenerator {
public var NonceBytes: Int { 16 }
}
class NonceGenerator24Byte: NonceGenerator {
public var NonceBytes: Int { 24 }
}
}
public extension Crypto.Size {
static let nonce16: Crypto.Size = Crypto.Size(id: "nonce16") { OpenGroupAPI.NonceGenerator16Byte().NonceBytes }
static let nonce24: Crypto.Size = Crypto.Size(id: "nonce24") { OpenGroupAPI.NonceGenerator24Byte().NonceBytes }
}
public extension Crypto.Action {
static func generateNonce16() -> Crypto.Action {
return Crypto.Action(id: "generateNonce16") { OpenGroupAPI.NonceGenerator16Byte().nonce() }
}
static func generateNonce24() -> Crypto.Action {
return Crypto.Action(id: "generateNonce24") { OpenGroupAPI.NonceGenerator24Byte().nonce() }
}
}
// MARK: - AeadXChaCha20Poly1305Ietf
public extension Crypto.Size {
static let aeadXChaCha20KeyBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20KeyBytes") {
Sodium().aead.xchacha20poly1305ietf.KeyBytes
}
static let aeadXChaCha20ABytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20ABytes") {
Sodium().aead.xchacha20poly1305ietf.ABytes
}
}
public extension Crypto.Action {
/// This method is the same as the standard AeadXChaCha20Poly1305Ietf `encrypt` method except it allows the
/// specification of a nonce which allows for deterministic behaviour with unit testing
static func encryptAeadXChaCha20(
message: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil,
using dependencies: Dependencies
) -> Crypto.Action {
return Crypto.Action(
id: "encryptAeadXChaCha20",
args: [message, secretKey, nonce, additionalData]
) {
guard secretKey.count == dependencies.crypto.size(.aeadXChaCha20KeyBytes) else { return nil }
var authenticatedCipherText = Bytes(
repeating: 0,
count: message.count + dependencies.crypto.size(.aeadXChaCha20ABytes)
)
var authenticatedCipherTextLen: UInt64 = 0
let result = crypto_aead_xchacha20poly1305_ietf_encrypt(
&authenticatedCipherText, &authenticatedCipherTextLen,
message, UInt64(message.count),
additionalData, UInt64(additionalData?.count ?? 0),
nil, nonce, secretKey
)
guard result == 0 else { return nil }
return authenticatedCipherText
}
}
static func decryptAeadXChaCha20(
authenticatedCipherText: Bytes,
secretKey: Bytes,
nonce: Bytes,
additionalData: Bytes? = nil
) -> Crypto.Action {
return Crypto.Action(
id: "decryptAeadXChaCha20",
args: [authenticatedCipherText, secretKey, nonce, additionalData]
) {
return Sodium().aead.xchacha20poly1305ietf.decrypt(
authenticatedCipherText: authenticatedCipherText,
secretKey: secretKey,
nonce: nonce,
additionalData: additionalData
)
}
}
}
// MARK: - Blinding
/// These extenion methods are used to generate a sign "blinded" messages
///
/// According to the Swift engineers the only situation when `UnsafeRawBufferPointer.baseAddress` is nil is when it's an
/// empty collection; as such our guard cases wihch return `-1` when unwrapping this value should never be hit and we can ignore
/// them as possible results.
///
/// For more information see:
/// https://forums.swift.org/t/when-is-unsafemutablebufferpointer-baseaddress-nil/32136/5
/// https://github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md#unsafebufferpointer
public extension Crypto.Action {
private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32
private static let noClampLength: Int = Int(Sodium.lib_crypto_scalarmult_ed25519_bytes()) // 32
private static let scalarMultLength: Int = Int(crypto_scalarmult_bytes()) // 32
fileprivate static let publicKeyLength: Int = Int(crypto_scalarmult_bytes()) // 32
fileprivate static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
/// 64-byte blake2b hash then reduce to get the blinding factor
static func generateBlindingFactor(
serverPublicKey: String,
using dependencies: Dependencies
) -> Crypto.Action {
return Crypto.Action(
id: "generateBlindingFactor",
args: [serverPublicKey]
) {
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
let serverPubKeyData: Data = Data(hex: serverPublicKey)
guard
!serverPubKeyData.isEmpty,
let serverPublicKeyHashBytes: Bytes = try? dependencies.crypto.perform(
.hash(message: [UInt8](serverPubKeyData), outputLength: 64)
)
else { return nil }
/// Reduce the server public key into an ed25519 scalar (`k`)
let kPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarLength)
_ = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let serverPublicKeyHashBaseAddress: UnsafePointer<UInt8> = serverPublicKeyHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(kPtr, serverPublicKeyHashBaseAddress)
return 0
}
return Data(bytes: kPtr, count: Crypto.Action.scalarLength).bytes
}
}
/// Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
/// same secret scalar secret (and so this is just the most convenient way to get 'a' out of
/// a sodium Ed25519 secret key)
fileprivate static func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes {
/// a = s.to_curve25519_private_key().encode()
let aPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarMultLength)
/// Looks like the `crypto_sign_ed25519_sk_to_curve25519` function can't actually fail so no need to verify the result
/// See: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L70
_ = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let secretKeyBaseAddress: UnsafePointer<UInt8> = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
return crypto_sign_ed25519_sk_to_curve25519(aPtr, secretKeyBaseAddress)
}
return Data(bytes: aPtr, count: Crypto.Action.scalarMultLength).bytes
}
/// Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
/// construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
/// pubkeys (this doesn't affect verification at all)
static func sogsSignature(
message: Bytes,
secretKey: Bytes,
blindedSecretKey ka: Bytes,
blindedPublicKey kA: Bytes
) -> Crypto.Action {
return Crypto.Action(
id: "sogsSignature",
args: [message, secretKey, ka, kA]
) {
/// H_rh = sha512(s.encode()).digest()[32:]
let H_rh: Bytes = Bytes(SHA512.hash(data: secretKey).suffix(32))
/// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
let combinedHashBytes: Bytes = SHA512.hash(data: H_rh + kA + message).bytes
let rPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarLength)
_ = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let combinedHashBaseAddress: UnsafePointer<UInt8> = combinedHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(rPtr, combinedHashBaseAddress)
return 0
}
/// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
let sig_RPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.noClampLength)
guard crypto_scalarmult_ed25519_base_noclamp(sig_RPtr, rPtr) == 0 else { return nil }
/// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
let sig_RBytes: Bytes = Data(bytes: sig_RPtr, count: Crypto.Action.noClampLength).bytes
let HRAMHashBytes: Bytes = SHA512.hash(data: sig_RBytes + kA + message).bytes
let HRAMPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarLength)
_ = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let HRAMHashBaseAddress: UnsafePointer<UInt8> = HRAMHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(HRAMPtr, HRAMHashBaseAddress)
return 0
}
/// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
let sig_sMulPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarLength)
let sig_sPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.scalarLength)
_ = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kaBaseAddress: UnsafePointer<UInt8> = kaPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_mul(sig_sMulPtr, HRAMPtr, kaBaseAddress)
Sodium.lib_crypto_core_ed25519_scalar_add(sig_sPtr, rPtr, sig_sMulPtr)
return 0
}
/// full_sig = sig_R + sig_s
return (Data(bytes: sig_RPtr, count: Crypto.Action.noClampLength).bytes + Data(bytes: sig_sPtr, count: Crypto.Action.scalarLength).bytes)
}
}
/// Combines two keys (`kA`)
static func combineKeys(
lhsKeyBytes: Bytes,
rhsKeyBytes: Bytes
) -> Crypto.Action {
return Crypto.Action(
id: "combineKeys",
args: [lhsKeyBytes, rhsKeyBytes]
) {
let combinedPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.noClampLength)
let result = rhsKeyBytes.withUnsafeBytes { (rhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
return lhsKeyBytes.withUnsafeBytes { (lhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
guard let lhsKeyBytesBaseAddress: UnsafePointer<UInt8> = lhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
guard let rhsKeyBytesBaseAddress: UnsafePointer<UInt8> = rhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
return Sodium.lib_crypto_scalarmult_ed25519_noclamp(combinedPtr, lhsKeyBytesBaseAddress, rhsKeyBytesBaseAddress)
}
}
/// Ensure the above worked
guard result == 0 else { return nil }
return Data(bytes: combinedPtr, count: Crypto.Action.noClampLength).bytes
}
}
/// Calculate a shared secret for a message from A to B:
///
/// BLAKE2b(a kB || kA || kB)
///
/// The receiver can calulate the same value via:
///
/// BLAKE2b(b kA || kA || kB)
static func sharedBlindedEncryptionKey(
secretKey: Bytes,
otherBlindedPublicKey: Bytes,
fromBlindedPublicKey kA: Bytes,
toBlindedPublicKey kB: Bytes,
using dependencies: Dependencies
) -> Crypto.Action {
return Crypto.Action(
id: "sharedBlindedEncryptionKey",
args: [secretKey, otherBlindedPublicKey, kA, kB]
) {
let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey)
let combinedKeyBytes: Bytes = try dependencies.crypto.perform(
.combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey)
)
return try dependencies.crypto.perform(
.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32)
)
}
}
}
public extension Crypto.KeyPairType {
/// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair`
static func blindedKeyPair(
serverPublicKey: String,
edKeyPair: KeyPair,
using dependencies: Dependencies
) -> Crypto.KeyPairType {
return Crypto.KeyPairType(
id: "blindedKeyPair",
args: [serverPublicKey, edKeyPair]
) {
guard
edKeyPair.publicKey.count == Crypto.Action.publicKeyLength,
edKeyPair.secretKey.count == Crypto.Action.secretKeyLength,
let kBytes: Bytes = try? dependencies.crypto.perform(
.generateBlindingFactor(serverPublicKey: serverPublicKey, using: dependencies)
)
else { return nil }
let aBytes: Bytes = Crypto.Action.generatePrivateKeyScalar(secretKey: edKeyPair.secretKey)
/// Generate the blinded key pair `ka`, `kA`
let kaPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.secretKeyLength)
let kAPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Crypto.Action.publicKeyLength)
_ = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in
return kBytes.withUnsafeBytes { (kPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kBaseAddress: UnsafePointer<UInt8> = kPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
guard let aBaseAddress: UnsafePointer<UInt8> = aPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_mul(kaPtr, kBaseAddress, aBaseAddress)
return 0
}
}
guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil }
return KeyPair(
publicKey: Data(bytes: kAPtr, count: Crypto.Action.publicKeyLength).bytes,
secretKey: Data(bytes: kaPtr, count: Crypto.Action.secretKeyLength).bytes
)
}
}
}
public extension Crypto.Verification {
/// This method should be used to check if a users standard sessionId matches a blinded one
static func sessionId(
_ standardSessionId: String,
matchesBlindedId blindedSessionId: String,
serverPublicKey: String,
using dependencies: Dependencies
) -> Crypto.Verification {
return Crypto.Verification(
id: "sessionId",
args: [standardSessionId, blindedSessionId, serverPublicKey]
) {
// Only support generating blinded keys for standard session ids
guard
let sessionId: SessionId = SessionId(from: standardSessionId),
sessionId.prefix == .standard,
let blindedId: SessionId = SessionId(from: blindedSessionId),
(
blindedId.prefix == .blinded15 ||
blindedId.prefix == .blinded25
),
let kBytes: Bytes = try? dependencies.crypto.perform(
.generateBlindingFactor(serverPublicKey: serverPublicKey, using: dependencies)
)
else { return false }
/// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what
/// Signal's XEd25519 conversion always uses)
///
/// Note: The below method is code we have exposed from the `curve25519_verify` method within the Curve25519 library
/// rather than custom code we have written
guard let xEd25519Key: Data = try? Ed25519.publicKey(from: Data(hex: sessionId.publicKey)) else { return false }
/// Blind the positive public key
guard
let pk1: Bytes = try? dependencies.crypto.perform(
.combineKeys(lhsKeyBytes: kBytes, rhsKeyBytes: xEd25519Key.bytes)
)
else { return false }
/// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2
/// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
let pk2: Bytes = (pk1[0..<31] + [(pk1[31] ^ 0b1000_0000)])
return (
SessionId(.blinded15, publicKey: pk1).publicKey == blindedId.publicKey ||
SessionId(.blinded15, publicKey: pk2).publicKey == blindedId.publicKey
)
}
}
}

View File

@ -69,7 +69,6 @@ public extension OpenGroupAPI {
info = HTTP.ResponseInfo(code: 0, headers: [:])
data = [:]
#endif
}
}
}

View File

@ -69,7 +69,7 @@ extension OpenGroupAPI.Message {
guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else {
throw HTTPError.parsingFailed
}
guard let dependencies: SMKDependencies = decoder.userInfo[Dependencies.userInfoKey] as? SMKDependencies else {
guard let dependencies: Dependencies = decoder.userInfo[Dependencies.userInfoKey] as? Dependencies else {
throw HTTPError.parsingFailed
}
@ -78,13 +78,21 @@ extension OpenGroupAPI.Message {
switch SessionId.Prefix(from: sender) {
case .blinded15, .blinded25:
guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else {
guard
dependencies.crypto.verify(
.signature(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes)
)
else {
SNLog("Ignoring message with invalid signature.")
throw HTTPError.parsingFailed
}
case .standard, .unblinded:
guard (try? dependencies.ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true else {
guard
dependencies.crypto.verify(
.signatureEd25519(signature, publicKey: publicKey, data: data)
)
else {
SNLog("Ignoring message with invalid signature.")
throw HTTPError.parsingFailed
}

View File

@ -31,7 +31,7 @@ public enum OpenGroupAPI {
server: String,
hasPerformedInitialPoll: Bool,
timeSinceLastPoll: TimeInterval,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
let lastInboxMessageId: Int64 = (try? OpenGroup
.select(.inboxLatestMessageId)
@ -143,7 +143,7 @@ public enum OpenGroupAPI {
_ db: Database,
server: String,
requests: [ErasedPreparedSendData],
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -173,7 +173,7 @@ public enum OpenGroupAPI {
_ db: Database,
server: String,
requests: [ErasedPreparedSendData],
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -202,7 +202,7 @@ public enum OpenGroupAPI {
_ db: Database,
server: String,
forceBlinded: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Capabilities> {
return try OpenGroupAPI
.prepareSendData(
@ -225,7 +225,7 @@ public enum OpenGroupAPI {
public static func preparedRooms(
_ db: Database,
server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Room]> {
return try OpenGroupAPI
.prepareSendData(
@ -244,7 +244,7 @@ public enum OpenGroupAPI {
_ db: Database,
for roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Room> {
return try OpenGroupAPI
.prepareSendData(
@ -267,7 +267,7 @@ public enum OpenGroupAPI {
lastUpdated: Int64,
for roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<RoomPollInfo> {
return try OpenGroupAPI
.prepareSendData(
@ -292,7 +292,7 @@ public enum OpenGroupAPI {
_ db: Database,
for roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<CapabilitiesAndRoomResponse> {
return try OpenGroupAPI
.preparedSequence(
@ -332,13 +332,18 @@ public enum OpenGroupAPI {
}
}
public typealias CapabilitiesAndRoomsResponse = (
capabilities: (info: ResponseInfoType, data: Capabilities),
rooms: (info: ResponseInfoType, data: [Room])
)
/// This is a convenience method which constructs a `/sequence` of the `capabilities` and `rooms` requests, refer to those
/// methods for the documented behaviour of each method
public static func preparedCapabilitiesAndRooms(
_ db: Database,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
) throws -> PreparedSendData<(capabilities: (info: ResponseInfoType, data: Capabilities), rooms: (info: ResponseInfoType, data: [Room]))> {
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<CapabilitiesAndRoomsResponse> {
return try OpenGroupAPI
.preparedSequence(
db,
@ -351,7 +356,7 @@ public enum OpenGroupAPI {
],
using: dependencies
)
.map { (info: ResponseInfoType, response: BatchResponse) -> (capabilities: (info: ResponseInfoType, data: Capabilities), rooms: (info: ResponseInfoType, data: [Room])) in
.map { (info: ResponseInfoType, response: BatchResponse) -> CapabilitiesAndRoomsResponse in
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (response[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
let maybeRooms: HTTP.BatchSubResponse<[Room]>? = response.data
.first(where: { key, _ in
@ -387,7 +392,7 @@ public enum OpenGroupAPI {
whisperTo: String?,
whisperMods: Bool,
fileIds: [String]?,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Message> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
throw OpenGroupAPIError.signingFailed
@ -419,7 +424,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Message> {
return try OpenGroupAPI
.prepareSendData(
@ -443,7 +448,7 @@ public enum OpenGroupAPI {
fileIds: [Int64]?,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
throw OpenGroupAPIError.signingFailed
@ -473,7 +478,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -497,7 +502,7 @@ public enum OpenGroupAPI {
_ db: Database,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
@ -526,7 +531,7 @@ public enum OpenGroupAPI {
messageId: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
@ -555,7 +560,7 @@ public enum OpenGroupAPI {
seqNo: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
@ -591,7 +596,7 @@ public enum OpenGroupAPI {
sessionId: String,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -615,7 +620,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
@ -646,7 +651,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionAddResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
@ -675,7 +680,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionRemoveResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
@ -705,7 +710,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionRemoveAllResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
@ -743,7 +748,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -766,7 +771,7 @@ public enum OpenGroupAPI {
id: Int64,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -788,7 +793,7 @@ public enum OpenGroupAPI {
_ db: Database,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -817,7 +822,7 @@ public enum OpenGroupAPI {
fileName: String? = nil,
to roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<FileUploadResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -849,7 +854,7 @@ public enum OpenGroupAPI {
fileId: String,
from roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Data> {
return try OpenGroupAPI
.prepareSendData(
@ -872,7 +877,7 @@ public enum OpenGroupAPI {
public static func preparedInbox(
_ db: Database,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
@ -893,7 +898,7 @@ public enum OpenGroupAPI {
_ db: Database,
id: Int64,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
@ -915,7 +920,7 @@ public enum OpenGroupAPI {
ciphertext: Data,
toInboxFor blindedSessionId: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> PreparedSendData<SendDirectMessageResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -939,7 +944,7 @@ public enum OpenGroupAPI {
public static func preparedOutbox(
_ db: Database,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
@ -960,7 +965,7 @@ public enum OpenGroupAPI {
_ db: Database,
id: Int64,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
@ -1013,7 +1018,7 @@ public enum OpenGroupAPI {
for timeout: TimeInterval? = nil,
from roomTokens: [String]? = nil,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -1062,7 +1067,7 @@ public enum OpenGroupAPI {
sessionId: String,
from roomTokens: [String]?,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
@ -1140,7 +1145,7 @@ public enum OpenGroupAPI {
visible: Bool,
for roomTokens: [String]?,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else {
throw HTTPError.generic
@ -1173,7 +1178,7 @@ public enum OpenGroupAPI {
sessionId: String,
in roomToken: String,
on server: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
return try OpenGroupAPI
.preparedSequence(
@ -1208,7 +1213,7 @@ public enum OpenGroupAPI {
for serverName: String,
fallbackSigningType signingType: SessionId.Prefix,
forceBlinded: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> (publicKey: String, signature: Bytes)? {
guard
let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db),
@ -1228,13 +1233,14 @@ public enum OpenGroupAPI {
// If we have no capabilities or if the server supports blinded keys then sign using the blinded key
if forceBlinded || capabilities.isEmpty || capabilities.contains(.blind) {
guard let blindedKeyPair: KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
return nil
}
guard let signatureResult: Bytes = dependencies.sodium.sogsSignature(message: messageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey) else {
return nil
}
guard
let blindedKeyPair: KeyPair = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: userEdKeyPair, using: dependencies)
),
let signatureResult: Bytes = try? dependencies.crypto.perform(
.sogsSignature(message: messageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey)
)
else { return nil }
return (
publicKey: SessionId(.blinded15, publicKey: blindedKeyPair.publicKey).hexString,
@ -1245,9 +1251,11 @@ public enum OpenGroupAPI {
// Otherwise sign using the fallback type
switch signingType {
case .unblinded:
guard let signatureResult: Bytes = dependencies.sign.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey) else {
return nil
}
guard
let signatureResult: Bytes = try? dependencies.crypto.perform(
.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey)
)
else { return nil }
return (
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
@ -1256,10 +1264,12 @@ public enum OpenGroupAPI {
// Default to using the 'standard' key
default:
guard let userKeyPair: KeyPair = Identity.fetchUserKeyPair(db) else { return nil }
guard let signatureResult: Bytes = try? dependencies.ed25519.sign(data: messageBytes, keyPair: userKeyPair) else {
return nil
}
guard
let userKeyPair: KeyPair = Identity.fetchUserKeyPair(db),
let signatureResult: Bytes = try? dependencies.crypto.perform(
.signEd25519(data: messageBytes, keyPair: userKeyPair)
)
else { return nil }
return (
publicKey: SessionId(.standard, publicKey: userKeyPair.publicKey).hexString,
@ -1275,7 +1285,7 @@ public enum OpenGroupAPI {
for serverName: String,
with serverPublicKey: String,
forceBlinded: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) -> URLRequest? {
guard let url: URL = request.url else { return nil }
@ -1283,12 +1293,12 @@ public enum OpenGroupAPI {
let path: String = url.path
.appending(url.query.map { value in "?\(value)" })
let method: String = (request.httpMethod ?? "GET")
let timestamp: Int = Int(floor(dependencies.date.timeIntervalSince1970))
let nonce: Data = Data(dependencies.nonceGenerator16.nonce())
let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970))
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
guard
!serverPublicKeyData.isEmpty,
let nonce: Data = (try? dependencies.crypto.perform(.generateNonce16())).map({ Data($0) }),
let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes
else { return nil }
@ -1296,7 +1306,7 @@ public enum OpenGroupAPI {
let bodyHash: Bytes? = {
guard let body: Data = request.httpBody else { return nil }
return dependencies.genericHash.hash(message: body.bytes, outputLength: 64)
return try? dependencies.crypto.perform(.hash(message: body.bytes, outputLength: 64))
}()
/// Generate the signature message
@ -1341,7 +1351,7 @@ public enum OpenGroupAPI {
responseType: R.Type,
forceBlinded: Bool = false,
timeout: TimeInterval = HTTP.defaultTimeout,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<R> {
let urlRequest: URLRequest = try request.generateUrlRequest()
let maybePublicKey: String? = try? OpenGroup
@ -1369,19 +1379,21 @@ public enum OpenGroupAPI {
/// This method takes in the `PreparedSendData<R>` and actually sends it to the API
public static func send<R>(
data: PreparedSendData<R>?,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, R), Error> {
guard let validData: PreparedSendData<R> = data else {
return Fail(error: OpenGroupAPIError.invalidPreparedData)
.eraseToAnyPublisher()
}
return dependencies.onionApi
.sendOnionRequest(
validData.request,
to: validData.server,
with: validData.publicKey,
timeout: validData.timeout
return dependencies.network
.send(
.onionRequest(
validData.request,
to: validData.server,
with: validData.publicKey,
timeout: validData.timeout
)
)
.decoded(with: validData, using: dependencies)
.eraseToAnyPublisher()

View File

@ -12,48 +12,17 @@ import SessionSnodeKit
public final class OpenGroupManager {
public typealias DefaultRoomInfo = (room: OpenGroupAPI.Room, existingImageData: Data?)
// MARK: - Cache
public class Cache: OGMMutableCacheType {
public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>?
public var groupImagePublishers: [String: AnyPublisher<Data, Error>] = [:]
public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
public var isPolling: Bool = false
/// Server URL to value
public var hasPerformedInitialPoll: [String: Bool] = [:]
public var timeSinceLastPoll: [String: TimeInterval] = [:]
fileprivate var _timeSinceLastOpen: TimeInterval?
public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
return storedTimeSinceLastOpen
}
guard let lastOpen: Date = dependencies.standardUserDefaults[.lastOpen] else {
_timeSinceLastOpen = .greatestFiniteMagnitude
return .greatestFiniteMagnitude
}
_timeSinceLastOpen = dependencies.date.timeIntervalSince(lastOpen)
return dependencies.date.timeIntervalSince(lastOpen)
}
public var pendingChanges: [OpenGroupAPI.PendingChange] = []
}
// MARK: - Variables
public static let shared: OpenGroupManager = OpenGroupManager()
// MARK: - Polling
public func startPolling(using dependencies: OGMDependencies = OGMDependencies()) {
public func startPolling(using dependencies: Dependencies = Dependencies()) {
// Run on the 'workQueue' to ensure any 'Atomic' access doesn't block the main thread
// on startup
OpenGroupAPI.workQueue.async {
guard !dependencies.cache.isPolling else { return }
OpenGroupAPI.workQueue.async(using: dependencies) {
guard !dependencies.caches[.openGroupManager].isPolling else { return }
let servers: Set<String> = dependencies.storage
.read { db in
@ -70,7 +39,7 @@ public final class OpenGroupManager {
.defaulting(to: [])
// Update the cache state and re-create all of the pollers
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.isPolling = true
cache.pollers = servers
.reduce(into: [:]) { result, server in
@ -80,13 +49,14 @@ public final class OpenGroupManager {
}
// Now that the pollers have been created actually start them
dependencies.cache.pollers.forEach { _, poller in poller.startIfNeeded(using: dependencies) }
dependencies.caches[.openGroupManager].pollers
.forEach { _, poller in poller.startIfNeeded(using: dependencies) }
}
}
public func stopPolling(using dependencies: OGMDependencies = OGMDependencies()) {
dependencies.mutableCache.mutate {
$0.pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
public func stopPolling(using dependencies: Dependencies = Dependencies()) {
dependencies.caches.mutate(cache: .openGroupManager) {
$0.pollers.forEach { _, openGroupPoller in openGroupPoller.stop() }
$0.pollers.removeAll()
$0.isPolling = false
}
@ -132,7 +102,13 @@ public final class OpenGroupManager {
return options.contains(serverHost)
}
public func hasExistingOpenGroup(_ db: Database, roomToken: String, server: String, publicKey: String, dependencies: OGMDependencies = OGMDependencies()) -> Bool {
public func hasExistingOpenGroup(
_ db: Database,
roomToken: String,
server: String,
publicKey: String,
using dependencies: Dependencies = Dependencies()
) -> Bool {
guard let serverUrl: URL = URL(string: server.lowercased()) else { return false }
let serverPort: String = OpenGroupManager.port(for: server, serverUrl: serverUrl)
@ -164,7 +140,7 @@ public final class OpenGroupManager {
}
// First check if there is no poller for the specified server
if Set(dependencies.cache.pollers.keys).intersection(serverOptions).isEmpty {
if Set(dependencies.caches[.openGroupManager].pollers.keys).intersection(serverOptions).isEmpty {
return false
}
@ -187,10 +163,10 @@ public final class OpenGroupManager {
server: String,
publicKey: String,
calledFromConfigHandling: Bool,
dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> Bool {
// If we are currently polling for this server and already have a TSGroupThread for this room the do nothing
if hasExistingOpenGroup(db, roomToken: roomToken, server: server, publicKey: publicKey, dependencies: dependencies) {
if hasExistingOpenGroup(db, roomToken: roomToken, server: server, publicKey: publicKey, using: dependencies) {
SNLog("Ignoring join open group attempt (already joined), user initiated: \(!calledFromConfigHandling)")
return false
}
@ -256,7 +232,7 @@ public final class OpenGroupManager {
server: String,
publicKey: String,
calledFromConfigHandling: Bool,
dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard successfullyAddedGroup else {
return Just(())
@ -311,7 +287,7 @@ public final class OpenGroupManager {
publicKey: publicKey,
for: roomToken,
on: targetServer,
dependencies: dependencies
using: dependencies
) {
resolver(Result.success(()))
}
@ -322,7 +298,7 @@ public final class OpenGroupManager {
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Failed to join open group.")
case .failure(let error): SNLog("Failed to join open group with error: \(error).")
}
}
)
@ -333,7 +309,7 @@ public final class OpenGroupManager {
_ db: Database,
openGroupId: String,
calledFromConfigHandling: Bool,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) {
let server: String? = try? OpenGroup
.select(.server)
@ -358,9 +334,9 @@ public final class OpenGroupManager {
.defaulting(to: 1)
if numActiveRooms == 1, let server: String = server?.lowercased() {
let poller = dependencies.cache.pollers[server]
let poller = dependencies.caches[.openGroupManager].pollers[server]
poller?.stop()
dependencies.mutableCache.mutate { $0.pollers[server] = nil }
dependencies.caches.mutate(cache: .openGroupManager) { $0.pollers[server] = nil }
}
// Remove all the data (everything should cascade delete)
@ -435,7 +411,7 @@ public final class OpenGroupManager {
for roomToken: String,
on server: String,
waitForImageToComplete: Bool = false,
dependencies: OGMDependencies = OGMDependencies(),
using dependencies: Dependencies,
completion: (() -> ())? = nil
) throws {
// Create the open group model and get or create the thread
@ -521,18 +497,19 @@ public final class OpenGroupManager {
}
}
db.afterNextTransactionNested { _ in
db.afterNextTransactionNested { reentrantDb in
// Dispatch async to the workQueue to prevent holding up the DBWrite thread from the
// above transaction
OpenGroupAPI.workQueue.async {
OpenGroupAPI.workQueue.async(using: dependencies) {
// Start the poller if needed
if dependencies.cache.pollers[server.lowercased()] == nil {
dependencies.mutableCache.mutate {
if dependencies.caches[.openGroupManager].pollers[server.lowercased()] == nil {
dependencies.caches.mutate(cache: .openGroupManager) {
$0.pollers[server.lowercased()]?.stop()
$0.pollers[server.lowercased()] = OpenGroupAPI.Poller(for: server.lowercased())
}
dependencies.cache.pollers[server.lowercased()]?.startIfNeeded(using: dependencies)
dependencies.caches[.openGroupManager].pollers[server.lowercased()]?
.startIfNeeded(using: dependencies)
}
/// Start downloading the room image (if we don't have one or it's been updated)
@ -553,8 +530,8 @@ public final class OpenGroupManager {
)
// Note: We need to subscribe and receive on different threads to ensure the
// logic in 'receiveValue' doesn't result in a reentrancy database issue
.subscribe(on: OpenGroupAPI.workQueue)
.receive(on: DispatchQueue.global(qos: .default))
.subscribe(on: OpenGroupAPI.workQueue, using: dependencies)
.receive(on: DispatchQueue.global(qos: .default), using: dependencies)
.sinkUntilComplete(
receiveCompletion: { _ in
if waitForImageToComplete {
@ -562,7 +539,7 @@ public final class OpenGroupManager {
}
},
receiveValue: { data in
dependencies.storage.write { db in
dependencies.storage.write(using: dependencies) { db in
_ = try OpenGroup
.filter(id: threadId)
.updateAll(db, OpenGroup.Columns.imageData.set(to: data))
@ -588,7 +565,7 @@ public final class OpenGroupManager {
messages: [OpenGroupAPI.Message],
for roomToken: String,
on server: String,
dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies
) {
guard let openGroup: OpenGroup = try? OpenGroup.fetchOne(db, id: OpenGroup.idFor(roomToken: roomToken, server: server)) else {
SNLog("Couldn't handle open group messages.")
@ -623,7 +600,7 @@ public final class OpenGroupManager {
openGroupServerPublicKey: openGroup.publicKey,
message: message,
data: data,
dependencies: dependencies
using: dependencies
)
if let messageInfo: MessageReceiveJob.Details.MessageInfo = processedMessage?.messageInfo {
@ -634,7 +611,7 @@ public final class OpenGroupManager {
message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
dependencies: dependencies
using: dependencies
)
largestValidSeqNo = max(largestValidSeqNo, message.seqNo)
}
@ -661,7 +638,7 @@ public final class OpenGroupManager {
db,
openGroupId: openGroup.id,
message: message,
associatedPendingChanges: dependencies.cache.pendingChanges
associatedPendingChanges: dependencies.caches[.openGroupManager].pendingChanges
.filter {
guard $0.server == server && $0.room == roomToken && $0.changeType == .reaction else {
return false
@ -672,7 +649,7 @@ public final class OpenGroupManager {
}
return false
},
dependencies: dependencies
using: dependencies
)
try MessageReceiver.handleOpenGroupReactions(
@ -708,7 +685,7 @@ public final class OpenGroupManager {
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: largestValidSeqNo))
// Update pendingChange cache based on the `largestValidSeqNo` value
dependencies.mutableCache.mutate {
dependencies.caches.mutate(cache: .openGroupManager) {
$0.pendingChanges = $0.pendingChanges
.filter { $0.seqNo == nil || $0.seqNo! > largestValidSeqNo }
}
@ -719,7 +696,7 @@ public final class OpenGroupManager {
messages: [OpenGroupAPI.DirectMessage],
fromOutbox: Bool,
on server: String,
dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies
) {
// Don't need to do anything if we have no messages (it's a valid case)
guard !messages.isEmpty else { return }
@ -762,7 +739,7 @@ public final class OpenGroupManager {
data: messageData,
isOutgoing: fromOutbox,
otherBlindedPublicKey: (fromOutbox ? message.recipient : message.sender),
dependencies: dependencies
using: dependencies
)
// We want to update the BlindedIdLookup cache with the message info so we can avoid using the
@ -788,7 +765,7 @@ public final class OpenGroupManager {
openGroupServer: server.lowercased(),
openGroupPublicKey: openGroup.publicKey,
isCheckingForOutbox: fromOutbox,
dependencies: dependencies
using: dependencies
)
}()
lookupCache[message.recipient] = lookup
@ -817,7 +794,7 @@ public final class OpenGroupManager {
message: messageInfo.message,
serverExpirationTimestamp: messageInfo.serverExpirationTimestamp,
associatedWithProto: try SNProtoContent.parseData(messageInfo.serializedProtoData),
dependencies: dependencies
using: dependencies
)
}
}
@ -846,7 +823,7 @@ public final class OpenGroupManager {
in roomToken: String,
on server: String,
type: OpenGroupAPI.PendingChange.ReactAction,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> OpenGroupAPI.PendingChange {
let pendingChange = OpenGroupAPI.PendingChange(
server: server,
@ -859,7 +836,7 @@ public final class OpenGroupManager {
)
)
dependencies.mutableCache.mutate {
dependencies.caches.mutate(cache: .openGroupManager) {
$0.pendingChanges.append(pendingChange)
}
@ -869,9 +846,9 @@ public final class OpenGroupManager {
public static func updatePendingChange(
_ pendingChange: OpenGroupAPI.PendingChange,
seqNo: Int64?,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) {
dependencies.mutableCache.mutate {
dependencies.caches.mutate(cache: .openGroupManager) {
if let index = $0.pendingChanges.firstIndex(of: pendingChange) {
$0.pendingChanges[index].seqNo = seqNo
}
@ -880,9 +857,9 @@ public final class OpenGroupManager {
public static func removePendingChange(
_ pendingChange: OpenGroupAPI.PendingChange,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) {
dependencies.mutableCache.mutate {
dependencies.caches.mutate(cache: .openGroupManager) {
if let index = $0.pendingChanges.firstIndex(of: pendingChange) {
$0.pendingChanges.remove(at: index)
}
@ -894,7 +871,7 @@ public final class OpenGroupManager {
_ db: Database? = nil,
capability: Capability.Variant,
on server: String?,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> Bool {
guard let server: String = server else { return false }
guard let db: Database = db else {
@ -919,7 +896,7 @@ public final class OpenGroupManager {
_ publicKey: String,
for roomToken: String?,
on server: String?,
using dependencies: OGMDependencies = OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> Bool {
guard let roomToken: String = roomToken, let server: String = server else { return false }
@ -943,7 +920,7 @@ public final class OpenGroupManager {
// Conveniently the logic for these different cases works in order so we can fallthrough each
// case with only minor efficiency losses
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
switch sessionId.prefix {
case .standard:
@ -967,10 +944,12 @@ public final class OpenGroupManager {
.filter(id: groupId)
.asRequest(of: String.self)
.fetchOne(db),
let blindedKeyPair: KeyPair = dependencies.sodium.blindedKeyPair(
serverPublicKey: openGroupPublicKey,
edKeyPair: userEdKeyPair,
genericHash: dependencies.genericHash
let blindedKeyPair: KeyPair = dependencies.crypto.generate(
.blindedKeyPair(
serverPublicKey: openGroupPublicKey,
edKeyPair: userEdKeyPair,
using: dependencies
)
)
else { return false }
guard
@ -1003,19 +982,16 @@ public final class OpenGroupManager {
}
@discardableResult public static func getDefaultRoomsIfNeeded(
using dependencies: OGMDependencies = OGMDependencies(
subscribeQueue: OpenGroupAPI.workQueue,
receiveQueue: OpenGroupAPI.workQueue
)
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[DefaultRoomInfo], Error> {
// Note: If we already have a 'defaultRoomsPromise' then there is no need to get it again
if let existingPublisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.cache.defaultRoomsPublisher {
if let existingPublisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.caches[.openGroupManager].defaultRoomsPublisher {
return existingPublisher
}
// Try to retrieve the default rooms 8 times
let publisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.storage
.readPublisher { db in
.readPublisher { db -> OpenGroupAPI.PreparedSendData<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
try OpenGroupAPI.preparedCapabilitiesAndRooms(
db,
on: OpenGroupAPI.defaultServer,
@ -1023,9 +999,9 @@ public final class OpenGroupManager {
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.subscribe(on: dependencies.subscribeQueue)
.receive(on: dependencies.receiveQueue)
.retry(8)
.subscribe(on: OpenGroupAPI.workQueue, using: dependencies)
.receive(on: OpenGroupAPI.workQueue, using: dependencies)
.retry(8, using: dependencies)
.map { info, response -> [DefaultRoomInfo]? in
dependencies.storage.write { db -> [DefaultRoomInfo] in
// Store the capabilities first
@ -1090,7 +1066,7 @@ public final class OpenGroupManager {
switch result {
case .finished: break
case .failure:
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.defaultRoomsPublisher = nil
}
}
@ -1099,7 +1075,7 @@ public final class OpenGroupManager {
.shareReplay(1)
.eraseToAnyPublisher()
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.defaultRoomsPublisher = publisher
}
@ -1114,9 +1090,7 @@ public final class OpenGroupManager {
for roomToken: String,
on server: String,
existingData: Data?,
using dependencies: OGMDependencies = OGMDependencies(
subscribeQueue: .global(qos: .background)
)
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Data, Error> {
// Normally the image for a given group is stored with the group thread, so it's only
// fetched once. However, on the join open group screen we show images for groups the
@ -1129,7 +1103,7 @@ public final class OpenGroupManager {
// there is one.
let threadId: String = OpenGroup.idFor(roomToken: roomToken, server: server)
let lastOpenGroupImageUpdate: Date? = dependencies.standardUserDefaults[.lastOpenGroupImageUpdate]
let now: Date = dependencies.date
let now: Date = dependencies.dateNow
let timeSinceLastUpdate: TimeInterval = (lastOpenGroupImageUpdate.map { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude)
let updateInterval: TimeInterval = (7 * 24 * 60 * 60)
let canUseExistingImage: Bool = (
@ -1143,14 +1117,14 @@ public final class OpenGroupManager {
.eraseToAnyPublisher()
}
if let publisher: AnyPublisher<Data, Error> = dependencies.cache.groupImagePublishers[threadId] {
if let publisher: AnyPublisher<Data, Error> = dependencies.caches[.openGroupManager].groupImagePublishers[threadId] {
return publisher
}
// Defer the actual download and run it on a separate thread to avoid blocking the calling thread
let publisher: AnyPublisher<Data, Error> = Deferred {
Future { resolver in
dependencies.subscribeQueue.async {
DispatchQueue.global(qos: .background).async(using: dependencies) {
// Hold on to the publisher until it has completed at least once
dependencies.storage
.readPublisher { db -> (Data?, OpenGroupAPI.PreparedSendData<Data>?) in
@ -1224,10 +1198,10 @@ public final class OpenGroupManager {
// Automatically subscribe for the roomImage download (want to download regardless of
// whether the upstream subscribes)
publisher
.subscribe(on: dependencies.subscribeQueue)
.subscribe(on: DispatchQueue.global(qos: .background), using: dependencies)
.sinkUntilComplete()
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.groupImagePublishers[threadId] = publisher
}
@ -1235,9 +1209,65 @@ public final class OpenGroupManager {
}
}
// MARK: - OpenGroupManager Cache
public extension OpenGroupManager {
class Cache: OGMCacheType {
public var defaultRoomsPublisher: AnyPublisher<[DefaultRoomInfo], Error>?
public var groupImagePublishers: [String: AnyPublisher<Data, Error>] = [:]
public var pollers: [String: OpenGroupAPI.Poller] = [:] // One for each server
public var isPolling: Bool = false
/// Server URL to value
public var hasPerformedInitialPoll: [String: Bool] = [:]
public var timeSinceLastPoll: [String: TimeInterval] = [:]
fileprivate var _timeSinceLastOpen: TimeInterval?
public func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval {
if let storedTimeSinceLastOpen: TimeInterval = _timeSinceLastOpen {
return storedTimeSinceLastOpen
}
guard let lastOpen: Date = dependencies.standardUserDefaults[.lastOpen] else {
_timeSinceLastOpen = .greatestFiniteMagnitude
return .greatestFiniteMagnitude
}
_timeSinceLastOpen = dependencies.dateNow.timeIntervalSince(lastOpen)
return dependencies.dateNow.timeIntervalSince(lastOpen)
}
public var pendingChanges: [OpenGroupAPI.PendingChange] = []
}
}
public extension Cache {
static let openGroupManager: CacheInfo.Config<OGMCacheType, OGMImmutableCacheType> = CacheInfo.create(
createInstance: { OpenGroupManager.Cache() },
mutableInstance: { $0 },
immutableInstance: { $0 }
)
}
// MARK: - OGMCacheType
public protocol OGMMutableCacheType: OGMCacheType {
/// This is a read-only version of the `OpenGroupManager.Cache` designed to avoid unintentionally mutating the instance in a
/// non-thread-safe way
public protocol OGMImmutableCacheType: ImmutableCacheType {
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get }
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get }
var pollers: [String: OpenGroupAPI.Poller] { get }
var isPolling: Bool { get }
var hasPerformedInitialPoll: [String: Bool] { get }
var timeSinceLastPoll: [String: TimeInterval] { get }
var pendingChanges: [OpenGroupAPI.PendingChange] { get }
}
public protocol OGMCacheType: OGMImmutableCacheType, MutableCacheType {
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get set }
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get set }
@ -1251,90 +1281,3 @@ public protocol OGMMutableCacheType: OGMCacheType {
func getTimeSinceLastOpen(using dependencies: Dependencies) -> TimeInterval
}
/// This is a read-only version of the `OGMMutableCacheType` designed to avoid unintentionally mutating the instance in a
/// non-thread-safe way
public protocol OGMCacheType {
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? { get }
var groupImagePublishers: [String: AnyPublisher<Data, Error>] { get }
var pollers: [String: OpenGroupAPI.Poller] { get }
var isPolling: Bool { get }
var hasPerformedInitialPoll: [String: Bool] { get }
var timeSinceLastPoll: [String: TimeInterval] { get }
var pendingChanges: [OpenGroupAPI.PendingChange] { get }
}
// MARK: - OGMDependencies
extension OpenGroupManager {
public class OGMDependencies: SMKDependencies {
/// These should not be accessed directly but rather via an instance of this type
private static let _cacheInstance: OGMMutableCacheType = OpenGroupManager.Cache()
private static let _cacheInstanceAccessQueue = DispatchQueue(label: "OGMCacheInstanceAccess")
internal var _mutableCache: Atomic<OGMMutableCacheType?>
public var mutableCache: Atomic<OGMMutableCacheType> {
get {
Dependencies.getMutableValueSettingIfNull(&_mutableCache) {
OGMDependencies._cacheInstanceAccessQueue.sync { OGMDependencies._cacheInstance }
}
}
}
public var cache: OGMCacheType {
get {
Dependencies.getValueSettingIfNull(&_mutableCache) {
OGMDependencies._cacheInstanceAccessQueue.sync { OGMDependencies._cacheInstance }
}
}
set {
guard let mutableValue: OGMMutableCacheType = newValue as? OGMMutableCacheType else { return }
_mutableCache.mutate { $0 = mutableValue }
}
}
public init(
subscribeQueue: DispatchQueue? = nil,
receiveQueue: DispatchQueue? = nil,
cache: OGMMutableCacheType? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: MutableGeneralCacheType? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_mutableCache = Atomic(cache)
super.init(
subscribeQueue: subscribeQueue,
receiveQueue: receiveQueue,
onionApi: onionApi,
generalCache: generalCache,
storage: storage,
scheduler: scheduler,
sodium: sodium,
box: box,
genericHash: genericHash,
sign: sign,
aeadXChaCha20Poly1305Ietf: aeadXChaCha20Poly1305Ietf,
ed25519: ed25519,
nonceGenerator16: nonceGenerator16,
nonceGenerator24: nonceGenerator24,
standardUserDefaults: standardUserDefaults,
date: date
)
}
}
}

View File

@ -1,25 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Sodium
public protocol NonceGenerator16ByteType {
var NonceBytes: Int { get }
func nonce() -> Array<UInt8>
}
public protocol NonceGenerator24ByteType {
var NonceBytes: Int { get }
func nonce() -> Array<UInt8>
}
extension OpenGroupAPI {
public class NonceGenerator16Byte: NonceGenerator, NonceGenerator16ByteType {
public var NonceBytes: Int { 16 }
}
public class NonceGenerator24Byte: NonceGenerator, NonceGenerator24ByteType {
public var NonceBytes: Int { 24 }
}
}

View File

@ -29,7 +29,7 @@ public extension OpenGroupAPI {
private let method: HTTPMethod
private let path: String
public let endpoint: Endpoint
fileprivate let batchEndpoints: [Endpoint]
internal let batchEndpoints: [Endpoint]
public let batchResponseTypes: [Decodable.Type]
/// The `jsonBodyEncoder` is used to simplify the encoding for `BatchRequest`
@ -185,7 +185,7 @@ public extension OpenGroupAPI.PreparedSendData {
public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error {
func decoded<R>(
with preparedData: OpenGroupAPI.PreparedSendData<R>,
using dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, R), Error> {
self
.tryMap { responseInfo, maybeData -> (ResponseInfoType, R) in

View File

@ -1,108 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Curve25519Kit
import SessionUtilitiesKit
public protocol SodiumType {
func getBox() -> BoxType
func getGenericHash() -> GenericHashType
func getSign() -> SignType
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType
func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes?
func blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair, genericHash: GenericHashType) -> KeyPair?
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes?
func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes?
func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes?
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool
}
public protocol AeadXChaCha20Poly1305IetfType {
var KeyBytes: Int { get }
var ABytes: Int { get }
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes?
}
public protocol Ed25519Type {
func sign(data: Bytes, keyPair: KeyPair) throws -> Bytes?
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool
}
public protocol BoxType {
func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes?
func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes?
}
public protocol GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes?
func hash(message: Bytes, outputLength: Int) -> Bytes?
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes?
}
public protocol SignType {
var Bytes: Int { get }
var PublicKeyBytes: Int { get }
func toX25519(ed25519PublicKey: Bytes) -> Bytes?
func signature(message: Bytes, secretKey: Bytes) -> Bytes?
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool
}
// MARK: - Default Values
extension GenericHashType {
func hash(message: Bytes) -> Bytes? { return hash(message: message, key: nil) }
func hashSaltPersonal(message: Bytes, outputLength: Int, salt: Bytes, personal: Bytes) -> Bytes? {
return hashSaltPersonal(message: message, outputLength: outputLength, key: nil, salt: salt, personal: personal)
}
}
extension AeadXChaCha20Poly1305IetfType {
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? {
return encrypt(message: message, secretKey: secretKey, nonce: nonce, additionalData: nil)
}
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? {
return decrypt(authenticatedCipherText: authenticatedCipherText, secretKey: secretKey, nonce: nonce, additionalData: nil)
}
}
// MARK: - Conformance
extension Sodium: SodiumType {
public func getBox() -> BoxType { return box }
public func getGenericHash() -> GenericHashType { return genericHash }
public func getSign() -> SignType { return sign }
public func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return aead.xchacha20poly1305ietf }
public func blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair) -> KeyPair? {
return blindedKeyPair(serverPublicKey: serverPublicKey, edKeyPair: edKeyPair, genericHash: getGenericHash())
}
}
extension Box: BoxType {}
extension GenericHash: GenericHashType {}
extension Sign: SignType {}
extension Aead.XChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {}
struct Ed25519Wrapper: Ed25519Type {
func sign(data: Bytes, keyPair: KeyPair) throws -> Bytes? {
let ecKeyPair: ECKeyPair = try ECKeyPair(
publicKeyData: Data(keyPair.publicKey),
privateKeyData: Data(keyPair.secretKey)
)
return try Ed25519.sign(Data(data), with: ecKeyPair).bytes
}
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool {
return try Ed25519.verifySignature(signature, publicKey: publicKey, data: data)
}
}

View File

@ -1,98 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionSnodeKit
import SessionUtilitiesKit
public class SMKDependencies: SSKDependencies {
internal var _sodium: Atomic<SodiumType?>
public var sodium: SodiumType {
get { Dependencies.getValueSettingIfNull(&_sodium) { Sodium() } }
set { _sodium.mutate { $0 = newValue } }
}
internal var _box: Atomic<BoxType?>
public var box: BoxType {
get { Dependencies.getValueSettingIfNull(&_box) { sodium.getBox() } }
set { _box.mutate { $0 = newValue } }
}
internal var _genericHash: Atomic<GenericHashType?>
public var genericHash: GenericHashType {
get { Dependencies.getValueSettingIfNull(&_genericHash) { sodium.getGenericHash() } }
set { _genericHash.mutate { $0 = newValue } }
}
internal var _sign: Atomic<SignType?>
public var sign: SignType {
get { Dependencies.getValueSettingIfNull(&_sign) { sodium.getSign() } }
set { _sign.mutate { $0 = newValue } }
}
internal var _aeadXChaCha20Poly1305Ietf: Atomic<AeadXChaCha20Poly1305IetfType?>
public var aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType {
get { Dependencies.getValueSettingIfNull(&_aeadXChaCha20Poly1305Ietf) { sodium.getAeadXChaCha20Poly1305Ietf() } }
set { _aeadXChaCha20Poly1305Ietf.mutate { $0 = newValue } }
}
internal var _ed25519: Atomic<Ed25519Type?>
public var ed25519: Ed25519Type {
get { Dependencies.getValueSettingIfNull(&_ed25519) { Ed25519Wrapper() } }
set { _ed25519.mutate { $0 = newValue } }
}
internal var _nonceGenerator16: Atomic<NonceGenerator16ByteType?>
public var nonceGenerator16: NonceGenerator16ByteType {
get { Dependencies.getValueSettingIfNull(&_nonceGenerator16) { OpenGroupAPI.NonceGenerator16Byte() } }
set { _nonceGenerator16.mutate { $0 = newValue } }
}
internal var _nonceGenerator24: Atomic<NonceGenerator24ByteType?>
public var nonceGenerator24: NonceGenerator24ByteType {
get { Dependencies.getValueSettingIfNull(&_nonceGenerator24) { OpenGroupAPI.NonceGenerator24Byte() } }
set { _nonceGenerator24.mutate { $0 = newValue } }
}
// MARK: - Initialization
public init(
subscribeQueue: DispatchQueue? = nil,
receiveQueue: DispatchQueue? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: MutableGeneralCacheType? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_sodium = Atomic(sodium)
_box = Atomic(box)
_genericHash = Atomic(genericHash)
_sign = Atomic(sign)
_aeadXChaCha20Poly1305Ietf = Atomic(aeadXChaCha20Poly1305Ietf)
_ed25519 = Atomic(ed25519)
_nonceGenerator16 = Atomic(nonceGenerator16)
_nonceGenerator24 = Atomic(nonceGenerator24)
super.init(
subscribeQueue: subscribeQueue,
receiveQueue: receiveQueue,
onionApi: onionApi,
generalCache: generalCache,
storage: storage,
scheduler: scheduler,
standardUserDefaults: standardUserDefaults,
date: date
)
}
}

View File

@ -11,6 +11,7 @@ public enum MessageSenderError: LocalizedError, Equatable {
case encryptionFailed
case noUsername
case attachmentsNotUploaded
case blindingFailed
// Closed groups
case noThread
@ -21,7 +22,10 @@ public enum MessageSenderError: LocalizedError, Equatable {
internal var isRetryable: Bool {
switch self {
case .invalidMessage, .protoConversionFailed, .invalidClosedGroupUpdate, .signingFailed, .encryptionFailed: return false
case .invalidMessage, .protoConversionFailed, .invalidClosedGroupUpdate,
.signingFailed, .encryptionFailed, .blindingFailed:
return false
default: return true
}
}
@ -36,6 +40,7 @@ public enum MessageSenderError: LocalizedError, Equatable {
case .encryptionFailed: return "Couldn't encrypt message."
case .noUsername: return "Missing username."
case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded."
case .blindingFailed: return "Couldn't blind the sender"
// Closed groups
case .noThread: return "Couldn't find a thread associated with the given group public key."
@ -58,6 +63,7 @@ public enum MessageSenderError: LocalizedError, Equatable {
case (.noThread, .noThread): return true
case (.noKeyPair, .noKeyPair): return true
case (.invalidClosedGroupUpdate, .invalidClosedGroupUpdate): return true
case (.blindingFailed, .blindingFailed): return true
case (.other(let lhsError), .other(let rhsError)):
// Not ideal but the best we can do

View File

@ -194,7 +194,11 @@ extension MessageReceiver {
// MARK: - Convenience
public static func handleIncomingCallOfferInBusyState(_ db: Database, message: CallMessage) throws {
public static func handleIncomingCallOfferInBusyState(
_ db: Database,
message: CallMessage,
using dependencies: Dependencies = Dependencies()
) throws {
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
guard
@ -229,7 +233,7 @@ extension MessageReceiver {
.inserted(db)
MessageSender.sendImmediate(
preparedSendData: try MessageSender
data: try MessageSender
.preparedSendData(
db,
message: CallMessage(
@ -242,8 +246,10 @@ extension MessageReceiver {
namespace: try Message.Destination
.from(db, threadId: thread.id, threadVariant: thread.variant)
.defaultNamespace,
interactionId: nil // Explicitly nil as it's a separate message from above
)
interactionId: nil, // Explicitly nil as it's a separate message from above
using: dependencies
),
using: dependencies
)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()

View File

@ -12,10 +12,11 @@ extension MessageReceiver {
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
message: ClosedGroupControlMessage
message: ClosedGroupControlMessage,
using dependencies: Dependencies = Dependencies()
) throws {
switch message.kind {
case .new: try handleNewClosedGroup(db, message: message)
case .new: try handleNewClosedGroup(db, message: message, using: dependencies)
case .encryptionKeyPair:
try handleClosedGroupEncryptionKeyPair(
@ -65,7 +66,11 @@ extension MessageReceiver {
// MARK: - Specific Handling
private static func handleNewClosedGroup(_ db: Database, message: ClosedGroupControlMessage) throws {
private static func handleNewClosedGroup(
_ db: Database,
message: ClosedGroupControlMessage,
using dependencies: Dependencies
) throws {
guard case let .new(publicKeyAsData, name, encryptionKeyPair, membersAsData, adminsAsData, expirationTimer) = message.kind else {
return
}
@ -112,7 +117,8 @@ extension MessageReceiver {
admins: adminsAsData.map { $0.toHexString() },
expirationTimer: expirationTimer,
formationTimestampMs: sentTimestamp,
calledFromConfigHandling: false
calledFromConfigHandling: false,
using: dependencies
)
}
@ -125,7 +131,8 @@ extension MessageReceiver {
admins: [String],
expirationTimer: UInt32,
formationTimestampMs: UInt64,
calledFromConfigHandling: Bool
calledFromConfigHandling: Bool,
using dependencies: Dependencies
) throws {
// With new closed groups we only want to create them if the admin creating the closed group is an
// approved contact (to prevent spam via closed groups getting around message requests if users are
@ -222,7 +229,7 @@ extension MessageReceiver {
}
// Start polling
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey)
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey, using: dependencies)
// Notify the PN server
let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey(db))

View File

@ -7,7 +7,11 @@ import SessionUIKit
import SessionUtilitiesKit
extension MessageReceiver {
internal static func handleLegacyConfigurationMessage(_ db: Database, message: ConfigurationMessage) throws {
internal static func handleLegacyConfigurationMessage(
_ db: Database,
message: ConfigurationMessage,
using dependencies: Dependencies
) throws {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard !SessionUtil.userConfigsEnabled(db) else {
TopBannerController.show(warning: .outdatedUserConfig)
@ -46,7 +50,8 @@ extension MessageReceiver {
)
}(),
sentTimestamp: messageSentTimestamp,
calledFromConfigHandling: true
calledFromConfigHandling: true,
using: dependencies
)
// Create a contact for the current user if needed (also force-approve the current user
@ -192,7 +197,8 @@ extension MessageReceiver {
admins: [String](closedGroup.admins),
expirationTimer: closedGroup.expirationTimer,
formationTimestampMs: message.sentTimestamp!,
calledFromConfigHandling: false // Legacy config isn't an issue
calledFromConfigHandling: false, // Legacy config isn't an issue
using: dependencies
)
}
}

View File

@ -10,9 +10,9 @@ extension MessageReceiver {
internal static func handleMessageRequestResponse(
_ db: Database,
message: MessageRequestResponse,
dependencies: SMKDependencies
using dependencies: Dependencies
) throws {
let userPublicKey = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let userPublicKey = getUserHexEncodedPublicKey(db, using: dependencies)
var blindedContactIds: [String] = []
// Ignore messages which were sent from the current user
@ -42,7 +42,8 @@ extension MessageReceiver {
fileName: nil
)
}(),
sentTimestamp: messageSentTimestamp
sentTimestamp: messageSentTimestamp,
using: dependencies
)
}
@ -73,11 +74,13 @@ extension MessageReceiver {
// If the sessionId matches the blindedId then this thread needs to be converted to an
// un-blinded thread
guard
dependencies.sodium.sessionId(
senderId,
matchesBlindedId: blindedIdLookup.blindedId,
serverPublicKey: blindedIdLookup.openGroupPublicKey,
genericHash: dependencies.genericHash
dependencies.crypto.verify(
.sessionId(
senderId,
matchesBlindedId: blindedIdLookup.blindedId,
serverPublicKey: blindedIdLookup.openGroupPublicKey,
using: dependencies
)
)
else { return }

View File

@ -13,7 +13,7 @@ extension MessageReceiver {
threadVariant: SessionThread.Variant,
message: VisibleMessage,
associatedWithProto proto: SNProtoContent,
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies = Dependencies()
) throws -> Int64 {
guard let sender: String = message.sender, let dataMessage = proto.dataMessage else {
throw MessageReceiverError.invalidMessage
@ -43,7 +43,8 @@ extension MessageReceiver {
fileName: nil
)
}(),
sentTimestamp: messageSentTimestamp
sentTimestamp: messageSentTimestamp,
using: dependencies
)
}
@ -64,7 +65,7 @@ extension MessageReceiver {
}
// Store the message variant so we can run variant-specific behaviours
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: threadId, variant: threadVariant, shouldBeVisible: nil)
let maybeOpenGroup: OpenGroup? = {
@ -90,10 +91,12 @@ extension MessageReceiver {
guard
let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db),
let blindedKeyPair: KeyPair = sodium.blindedKeyPair(
serverPublicKey: openGroup.publicKey,
edKeyPair: userEdKeyPair,
genericHash: sodium.genericHash
let blindedKeyPair: KeyPair = try? dependencies.crypto.generate(
.blindedKeyPair(
serverPublicKey: openGroup.publicKey,
edKeyPair: userEdKeyPair,
using: dependencies
)
)
else { return .standardIncoming }
@ -163,7 +166,8 @@ extension MessageReceiver {
db,
threadId: thread.id,
body: message.text,
quoteAuthorId: dataMessage.quote?.author
quoteAuthorId: dataMessage.quote?.author,
using: dependencies
),
// Note: Ensure we don't ever expire open group messages
expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ?
@ -294,7 +298,7 @@ extension MessageReceiver {
.appending(quote?.attachmentId)
.appending(linkPreview?.attachmentId)
.forEach { attachmentId in
JobRunner.add(
dependencies.jobRunner.add(
db,
job: Job(
variant: .attachmentDownload,
@ -304,7 +308,8 @@ extension MessageReceiver {
attachmentId: attachmentId
)
),
canStartJob: isMainAppActive
canStartJob: isMainAppActive,
using: dependencies
)
}
}

View File

@ -4,7 +4,6 @@ import Foundation
import Combine
import GRDB
import Sodium
import Curve25519Kit
import SessionUtilitiesKit
import SessionSnodeKit
@ -13,21 +12,21 @@ extension MessageSender {
public static func createClosedGroup(
name: String,
members: Set<String>
members: Set<String>,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<SessionThread, Error> {
Storage.shared
dependencies.storage
.writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData]) in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
var members: Set<String> = members
// Generate the group's two keys
guard
let groupKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()),
let encryptionKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair())
else { throw MessageSenderError.noKeyPair }
// Generate the group's public key
let groupKeyPair: ECKeyPair = Curve25519.generateKeyPair()
let groupPublicKey: String = KeyPair(
publicKey: groupKeyPair.publicKey.bytes,
secretKey: groupKeyPair.privateKey.bytes
).hexEncodedPublicKey // Includes the 'SessionId.Prefix.standard' prefix
// Generate the key pair that'll be used for encryption and decryption
let encryptionKeyPair: ECKeyPair = Curve25519.generateKeyPair()
// Includes the 'SessionId.Prefix.standard' prefix
let groupPublicKey: String = groupKeyPair.hexEncodedPublicKey
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
var members: Set<String> = members
// Create the group
members.insert(userPublicKey) // Ensure the current user is included in the member list
@ -49,8 +48,8 @@ extension MessageSender {
let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
try ClosedGroupKeyPair(
threadId: groupPublicKey,
publicKey: encryptionKeyPair.publicKey,
secretKey: encryptionKeyPair.privateKey,
publicKey: Data(encryptionKeyPair.publicKey),
secretKey: Data(encryptionKeyPair.secretKey),
receivedTimestamp: latestKeyPairReceivedTimestamp
).insert(db)
@ -78,8 +77,8 @@ extension MessageSender {
db,
groupPublicKey: groupPublicKey,
name: name,
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey),
latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey),
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
members: members,
@ -94,10 +93,7 @@ extension MessageSender {
kind: .new(
publicKey: Data(hex: groupPublicKey),
name: name,
encryptionKeyPair: KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
),
encryptionKeyPair: encryptionKeyPair,
members: membersAsData,
admins: adminsAsData,
expirationTimer: 0
@ -108,7 +104,8 @@ extension MessageSender {
),
to: .contact(publicKey: memberId),
namespace: Message.Destination.contact(publicKey: memberId).defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
}
@ -119,7 +116,7 @@ extension MessageSender {
.MergeMany(
// Send a closed group update message to all members individually
memberSendData
.map { MessageSender.sendImmediate(preparedSendData: $0) }
.map { MessageSender.sendImmediate(data: $0, using: dependencies) }
.appending(
// Notify the PN server
PushNotificationAPI.performOperation(
@ -135,7 +132,7 @@ extension MessageSender {
.handleEvents(
receiveOutput: { thread in
// Start polling
ClosedGroupPoller.shared.startIfNeeded(for: thread.id)
ClosedGroupPoller.shared.startIfNeeded(for: thread.id, using: dependencies)
}
)
.eraseToAnyPublisher()
@ -151,21 +148,25 @@ extension MessageSender {
targetMembers: Set<String>,
userPublicKey: String,
allGroupMembers: [GroupMember],
closedGroup: ClosedGroup
closedGroup: ClosedGroup,
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) else {
return Fail(error: MessageSenderError.invalidClosedGroupUpdate)
.eraseToAnyPublisher()
}
return Storage.shared
return dependencies.storage
.readPublisher { db -> (ClosedGroupKeyPair, MessageSender.PreparedSendData) in
// Generate the new encryption key pair
let legacyNewKeyPair: ECKeyPair = Curve25519.generateKeyPair()
guard let legacyNewKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()) else {
throw MessageSenderError.noKeyPair
}
let newKeyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
threadId: closedGroup.threadId,
publicKey: legacyNewKeyPair.publicKey,
secretKey: legacyNewKeyPair.privateKey,
publicKey: Data(legacyNewKeyPair.publicKey),
secretKey: Data(legacyNewKeyPair.secretKey),
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
)
@ -193,7 +194,8 @@ extension MessageSender {
encryptedKeyPair: try MessageSender.encryptWithSessionProtocol(
db,
plaintext: plaintext,
for: memberPublicKey
for: memberPublicKey,
using: dependencies
)
)
}
@ -204,20 +206,21 @@ extension MessageSender {
namespace: try Message.Destination
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
.defaultNamespace,
interactionId: nil
interactionId: nil,
using: dependencies
)
return (newKeyPair, sendData)
}
.flatMap { newKeyPair, sendData -> AnyPublisher<ClosedGroupKeyPair, Error> in
MessageSender.sendImmediate(preparedSendData: sendData)
MessageSender.sendImmediate(data: sendData, using: dependencies)
.map { _ in newKeyPair }
.eraseToAnyPublisher()
}
.handleEvents(
receiveOutput: { newKeyPair in
/// Store it **after** having sent out the message to the group
Storage.shared.write { db in
dependencies.storage.write { db in
try newKeyPair.insert(db)
// Update libSession
@ -251,11 +254,12 @@ extension MessageSender {
public static func update(
groupPublicKey: String,
with members: Set<String>,
name: String
name: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return Storage.shared
.writePublisher { db -> (String, ClosedGroup, [GroupMember], Set<String>) in
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
// Get the group, check preconditions & prepare
guard (try? SessionThread.exists(db, id: groupPublicKey)) == true else {
@ -292,7 +296,8 @@ extension MessageSender {
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
interactionId: interactionId,
threadId: groupPublicKey,
threadVariant: .legacyGroup
threadVariant: .legacyGroup,
using: dependencies
)
// Update libSession
@ -321,7 +326,8 @@ extension MessageSender {
addedMembers: addedMembers,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
closedGroup: closedGroup,
using: dependencies
)
}
catch {
@ -348,7 +354,8 @@ extension MessageSender {
removedMembers: removedMembers,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
closedGroup: closedGroup,
using: dependencies
)
.catch { _ in Fail(error: MessageSenderError.invalidClosedGroupUpdate).eraseToAnyPublisher() }
.eraseToAnyPublisher()
@ -364,7 +371,8 @@ extension MessageSender {
addedMembers: Set<String>,
userPublicKey: String,
allGroupMembers: [GroupMember],
closedGroup: ClosedGroup
closedGroup: ClosedGroup,
using dependencies: Dependencies
) throws {
guard let disappearingMessagesConfig: DisappearingMessagesConfiguration = try DisappearingMessagesConfiguration.fetchOne(db, id: closedGroup.threadId) else {
throw StorageError.objectNotFound
@ -419,7 +427,8 @@ extension MessageSender {
),
interactionId: interactionId,
threadId: closedGroup.threadId,
threadVariant: .legacyGroup
threadVariant: .legacyGroup,
using: dependencies
)
try addedMembers.forEach { member in
@ -446,7 +455,8 @@ extension MessageSender {
),
interactionId: nil,
threadId: member,
threadVariant: .contact
threadVariant: .contact,
using: dependencies
)
// Add the users to the group
@ -469,7 +479,8 @@ extension MessageSender {
removedMembers: Set<String>,
userPublicKey: String,
allGroupMembers: [GroupMember],
closedGroup: ClosedGroup
closedGroup: ClosedGroup,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard !removedMembers.contains(userPublicKey) else {
SNLog("Invalid closed group update.")
@ -490,7 +501,7 @@ extension MessageSender {
.map { $0.profileId }
let members: Set<String> = Set(groupMemberIds).subtracting(removedMembers)
return Storage.shared
return dependencies.storage
.writePublisher { db in
// Update zombie & member list
try GroupMember
@ -535,16 +546,18 @@ extension MessageSender {
namespace: try Message.Destination
.from(db, threadId: closedGroup.threadId, threadVariant: .legacyGroup)
.defaultNamespace,
interactionId: interactionId
interactionId: interactionId,
using: dependencies
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.flatMap { _ -> AnyPublisher<Void, Error> in
MessageSender.generateAndSendNewEncryptionKeyPair(
targetMembers: members,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
closedGroup: closedGroup,
using: dependencies
)
}
.eraseToAnyPublisher()
@ -561,9 +574,10 @@ extension MessageSender {
public static func leave(
_ db: Database,
groupPublicKey: String,
deleteThread: Bool
deleteThread: Bool,
using dependencies: Dependencies = Dependencies()
) throws {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
// Notify the user
let interaction: Interaction = try Interaction(
@ -574,7 +588,7 @@ extension MessageSender {
timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db)
JobRunner.upsert(
dependencies.jobRunner.upsert(
db,
job: Job(
variant: .groupLeaving,
@ -583,14 +597,17 @@ extension MessageSender {
details: GroupLeavingJob.Details(
deleteThread: deleteThread
)
)
),
canStartJob: true,
using: dependencies
)
}
public static func sendLatestEncryptionKeyPair(
_ db: Database,
to publicKey: String,
for groupPublicKey: String
for groupPublicKey: String,
using dependencies: Dependencies = Dependencies()
) {
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
return SNLog("Couldn't send key pair for nonexistent closed group.")
@ -626,7 +643,8 @@ extension MessageSender {
let ciphertext = try MessageSender.encryptWithSessionProtocol(
db,
plaintext: plaintext,
for: publicKey
for: publicKey,
using: dependencies
)
SNLog("Sending latest encryption key pair to: \(publicKey).")
@ -645,7 +663,8 @@ extension MessageSender {
),
interactionId: nil,
threadId: thread.id,
threadVariant: thread.variant
threadVariant: thread.variant,
using: dependencies
)
}
catch {}

View File

@ -6,18 +6,24 @@ import Sodium
import SessionUtilitiesKit
extension MessageReceiver {
internal static func decryptWithSessionProtocol(ciphertext: Data, using x25519KeyPair: KeyPair, dependencies: SMKDependencies = SMKDependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) {
let recipientX25519PrivateKey = x25519KeyPair.secretKey
let recipientX25519PublicKey = x25519KeyPair.publicKey
let signatureSize = dependencies.sign.Bytes
let ed25519PublicKeySize = dependencies.sign.PublicKeyBytes
internal static func decryptWithSessionProtocol(
ciphertext: Data,
using x25519KeyPair: KeyPair,
using dependencies: Dependencies = Dependencies()
) throws -> (plaintext: Data, senderX25519PublicKey: String) {
let recipientX25519PrivateKey: Bytes = x25519KeyPair.secretKey
let recipientX25519PublicKey: Bytes = x25519KeyPair.publicKey
let signatureSize: Int = dependencies.crypto.size(.signature)
let ed25519PublicKeySize: Int = dependencies.crypto.size(.publicKey)
// 1. ) Decrypt the message
guard
let plaintextWithMetadata = dependencies.box.open(
anonymousCipherText: Bytes(ciphertext),
recipientPublicKey: Box.PublicKey(Bytes(recipientX25519PublicKey)),
recipientSecretKey: Bytes(recipientX25519PrivateKey)
let plaintextWithMetadata = try? dependencies.crypto.perform(
.open(
anonymousCipherText: Bytes(ciphertext),
recipientPublicKey: Box.PublicKey(Bytes(recipientX25519PublicKey)),
recipientSecretKey: Bytes(recipientX25519PrivateKey)
)
),
plaintextWithMetadata.count > (signatureSize + ed25519PublicKeySize)
else {
@ -32,79 +38,100 @@ extension MessageReceiver {
// 3. ) Verify the signature
let verificationData = plaintext + senderED25519PublicKey + recipientX25519PublicKey
guard dependencies.sign.verify(message: verificationData, publicKey: senderED25519PublicKey, signature: signature) else {
throw MessageReceiverError.invalidSignature
}
guard
dependencies.crypto.verify(
.signature(message: verificationData, publicKey: senderED25519PublicKey, signature: signature)
)
else { throw MessageReceiverError.invalidSignature }
// 4. ) Get the sender's X25519 public key
guard let senderX25519PublicKey = dependencies.sign.toX25519(ed25519PublicKey: senderED25519PublicKey) else {
throw MessageReceiverError.decryptionFailed
}
guard
let senderX25519PublicKey = try? dependencies.crypto.perform(
.toX25519(ed25519PublicKey: senderED25519PublicKey)
)
else { throw MessageReceiverError.decryptionFailed }
return (Data(plaintext), SessionId(.standard, publicKey: senderX25519PublicKey).hexString)
}
internal static func decryptWithSessionBlindingProtocol(data: Data, isOutgoing: Bool, otherBlindedPublicKey: String, with openGroupPublicKey: String, userEd25519KeyPair: KeyPair, using dependencies: SMKDependencies = SMKDependencies()) throws -> (plaintext: Data, senderX25519PublicKey: String) {
internal static func decryptWithSessionBlindingProtocol(
data: Data,
isOutgoing: Bool,
otherBlindedPublicKey: String,
with openGroupPublicKey: String,
userEd25519KeyPair: KeyPair,
using dependencies: Dependencies = Dependencies()
) throws -> (plaintext: Data, senderX25519PublicKey: String) {
/// Ensure the data is at least long enough to have the required components
guard
data.count > (dependencies.nonceGenerator24.NonceBytes + 2),
let blindedKeyPair = dependencies.sodium.blindedKeyPair(
serverPublicKey: openGroupPublicKey,
edKeyPair: userEd25519KeyPair,
genericHash: dependencies.genericHash
data.count > (dependencies.crypto.size(.nonce24) + 2),
let blindedKeyPair = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, using: dependencies)
)
else { throw MessageReceiverError.decryptionFailed }
/// Step one: calculate the shared encryption key, receiving from A to B
let otherKeyBytes: Bytes = Data(hex: otherBlindedPublicKey.removingIdPrefixIfNeeded()).bytes
let kA: Bytes = (isOutgoing ? blindedKeyPair.publicKey : otherKeyBytes)
guard let dec_key: Bytes = dependencies.sodium.sharedBlindedEncryptionKey(
secretKey: userEd25519KeyPair.secretKey,
otherBlindedPublicKey: otherKeyBytes,
fromBlindedPublicKey: kA,
toBlindedPublicKey: (isOutgoing ? otherKeyBytes : blindedKeyPair.publicKey),
genericHash: dependencies.genericHash
) else {
throw MessageReceiverError.decryptionFailed
}
guard
let dec_key: Bytes = try? dependencies.crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: userEd25519KeyPair.secretKey,
otherBlindedPublicKey: otherKeyBytes,
fromBlindedPublicKey: kA,
toBlindedPublicKey: (isOutgoing ? otherKeyBytes : blindedKeyPair.publicKey),
using: dependencies
)
)
else { throw MessageReceiverError.decryptionFailed }
/// v, ct, nc = data[0], data[1:-24], data[-24:]
let version: UInt8 = data.bytes[0]
let ciphertext: Bytes = Bytes(data.bytes[1..<(data.count - dependencies.nonceGenerator24.NonceBytes)])
let nonce: Bytes = Bytes(data.bytes[(data.count - dependencies.nonceGenerator24.NonceBytes)..<data.count])
let ciphertext: Bytes = Bytes(data.bytes[1..<(data.count - dependencies.crypto.size(.nonce24))])
let nonce: Bytes = Bytes(data.bytes[(data.count - dependencies.crypto.size(.nonce24))..<data.count])
/// Make sure our encryption version is okay
guard version == 0 else { throw MessageReceiverError.decryptionFailed }
/// Decrypt
guard let innerBytes: Bytes = dependencies.aeadXChaCha20Poly1305Ietf.decrypt(authenticatedCipherText: ciphertext, secretKey: dec_key, nonce: nonce) else {
throw MessageReceiverError.decryptionFailed
}
guard
let innerBytes: Bytes = try? dependencies.crypto.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: ciphertext,
secretKey: dec_key,
nonce: nonce
)
)
else { throw MessageReceiverError.decryptionFailed }
/// Ensure the length is correct
guard innerBytes.count > dependencies.sign.PublicKeyBytes else { throw MessageReceiverError.decryptionFailed }
guard innerBytes.count > dependencies.crypto.size(.publicKey) else { throw MessageReceiverError.decryptionFailed }
/// Split up: the last 32 bytes are the sender's *unblinded* ed25519 key
let plaintext: Bytes = Bytes(innerBytes[
0...(innerBytes.count - 1 - dependencies.sign.PublicKeyBytes)
0...(innerBytes.count - 1 - dependencies.crypto.size(.publicKey))
])
let sender_edpk: Bytes = Bytes(innerBytes[
(innerBytes.count - dependencies.sign.PublicKeyBytes)...(innerBytes.count - 1)
(innerBytes.count - dependencies.crypto.size(.publicKey))...(innerBytes.count - 1)
])
/// Verify that the inner sender_edpk (A) yields the same outer kA we got with the message
guard let blindingFactor: Bytes = dependencies.sodium.generateBlindingFactor(serverPublicKey: openGroupPublicKey, genericHash: dependencies.genericHash) else {
throw MessageReceiverError.invalidSignature
}
guard let sharedSecret: Bytes = dependencies.sodium.combineKeys(lhsKeyBytes: blindingFactor, rhsKeyBytes: sender_edpk) else {
throw MessageReceiverError.invalidSignature
}
guard kA == sharedSecret else { throw MessageReceiverError.invalidSignature }
guard
let blindingFactor: Bytes = try? dependencies.crypto.perform(
.generateBlindingFactor(serverPublicKey: openGroupPublicKey, using: dependencies)
),
let sharedSecret: Bytes = try? dependencies.crypto.perform(
.combineKeys(lhsKeyBytes: blindingFactor, rhsKeyBytes: sender_edpk)
),
kA == sharedSecret
else { throw MessageReceiverError.invalidSignature }
/// Get the sender's X25519 public key
guard let senderSessionIdBytes: Bytes = dependencies.sign.toX25519(ed25519PublicKey: sender_edpk) else {
throw MessageReceiverError.decryptionFailed
}
guard
let senderSessionIdBytes: Bytes = try? dependencies.crypto.perform(
.toX25519(ed25519PublicKey: sender_edpk)
)
else { throw MessageReceiverError.decryptionFailed }
return (Data(plaintext), SessionId(.standard, publicKey: senderSessionIdBytes).hexString)
}

View File

@ -18,9 +18,9 @@ public enum MessageReceiver {
openGroupServerPublicKey: String?,
isOutgoing: Bool? = nil,
otherBlindedPublicKey: String? = nil,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> (Message, SNProtoContent, String, SessionThread.Variant) {
let userPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let isOpenGroupMessage: Bool = (openGroupId != nil)
// Decrypt the contents
@ -183,7 +183,7 @@ public enum MessageReceiver {
message: Message,
serverExpirationTimestamp: TimeInterval?,
associatedWithProto proto: SNProtoContent,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws {
// Check if the message requires an existing conversation (if it does and the conversation isn't in
// the config then the message will be dropped)
@ -198,7 +198,7 @@ public enum MessageReceiver {
message: message,
threadId: threadId,
threadVariant: threadVariant,
dependencies: dependencies
using: dependencies
)
switch message {
@ -222,7 +222,8 @@ public enum MessageReceiver {
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
message: message,
using: dependencies
)
case let message as DataExtractionNotification:
@ -242,7 +243,11 @@ public enum MessageReceiver {
)
case let message as ConfigurationMessage:
try MessageReceiver.handleLegacyConfigurationMessage(db, message: message)
try MessageReceiver.handleLegacyConfigurationMessage(
db,
message: message,
using: dependencies
)
case let message as UnsendRequest:
try MessageReceiver.handleUnsendRequest(
@ -264,7 +269,7 @@ public enum MessageReceiver {
try MessageReceiver.handleMessageRequestResponse(
db,
message: message,
dependencies: dependencies
using: dependencies
)
case let message as VisibleMessage:
@ -360,7 +365,7 @@ public enum MessageReceiver {
message: Message,
threadId: String,
threadVariant: SessionThread.Variant,
dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws {
switch message {
case is ReadReceipt: return // No visible artifact created so better to keep for more reliable read states
@ -369,7 +374,7 @@ public enum MessageReceiver {
}
// Determine the state of the conversation and the validity of the message
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let conversationVisibleInConfig: Bool = SessionUtil.conversationInConfig(
db,
threadId: threadId,

View File

@ -14,7 +14,8 @@ extension MessageSender {
interaction: Interaction,
threadId: String,
threadVariant: SessionThread.Variant,
isSyncMessage: Bool = false
isSyncMessage: Bool = false,
using dependencies: Dependencies
) throws {
// Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
@ -26,7 +27,8 @@ extension MessageSender {
threadId: threadId,
interactionId: interactionId,
to: try Message.Destination.from(db, threadId: threadId, threadVariant: threadVariant),
isSyncMessage: isSyncMessage
isSyncMessage: isSyncMessage,
using: dependencies
)
}
@ -36,7 +38,8 @@ extension MessageSender {
interactionId: Int64?,
threadId: String,
threadVariant: SessionThread.Variant,
isSyncMessage: Bool = false
isSyncMessage: Bool = false,
using dependencies: Dependencies
) throws {
send(
db,
@ -44,7 +47,8 @@ extension MessageSender {
threadId: threadId,
interactionId: interactionId,
to: try Message.Destination.from(db, threadId: threadId, threadVariant: threadVariant),
isSyncMessage: isSyncMessage
isSyncMessage: isSyncMessage,
using: dependencies
)
}
@ -54,7 +58,8 @@ extension MessageSender {
threadId: String?,
interactionId: Int64?,
to destination: Message.Destination,
isSyncMessage: Bool = false
isSyncMessage: Bool = false,
using dependencies: Dependencies
) {
// If it's a sync message then we need to make some slight tweaks before sending so use the proper
// sync message sending process instead of the standard process
@ -65,12 +70,13 @@ extension MessageSender {
destination: destination,
threadId: threadId,
interactionId: interactionId,
isAlreadySyncMessage: false
isAlreadySyncMessage: false,
using: dependencies
)
return
}
JobRunner.add(
dependencies.jobRunner.add(
db,
job: Job(
variant: .messageSend,
@ -81,7 +87,9 @@ extension MessageSender {
message: message,
isSyncMessage: isSyncMessage
)
)
),
canStartJob: true,
using: dependencies
)
}
@ -91,7 +99,8 @@ extension MessageSender {
_ db: Database,
interaction: Interaction,
threadId: String,
threadVariant: SessionThread.Variant
threadVariant: SessionThread.Variant,
using dependencies: Dependencies
) throws -> PreparedSendData {
// Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
@ -104,11 +113,15 @@ extension MessageSender {
namespace: try Message.Destination
.from(db, threadId: threadId, threadVariant: threadVariant)
.defaultNamespace,
interactionId: interactionId
interactionId: interactionId,
using: dependencies
)
}
public static func performUploadsIfNeeded(preparedSendData: PreparedSendData) -> AnyPublisher<PreparedSendData, Error> {
public static func performUploadsIfNeeded(
preparedSendData: PreparedSendData,
using dependencies: Dependencies
) -> AnyPublisher<PreparedSendData, Error> {
// We need an interactionId in order for a message to have uploads
guard let interactionId: Int64 = preparedSendData.interactionId else {
return Just(preparedSendData)
@ -127,7 +140,7 @@ extension MessageSender {
}
}()
return Storage.shared
return dependencies.storage
.readPublisher { db -> (attachments: [Attachment], openGroup: OpenGroup?) in
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
.stateInfo(interactionId: interactionId, state: .uploading)
@ -162,7 +175,8 @@ extension MessageSender {
to: (
openGroup.map { Attachment.Destination.openGroup($0) } ??
.fileServer
)
),
using: dependencies
)
}
)

View File

@ -10,7 +10,7 @@ extension MessageSender {
_ db: Database,
plaintext: Data,
for recipientHexEncodedX25519PublicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> Data {
guard let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw MessageSenderError.noUserED25519KeyPair
@ -19,14 +19,21 @@ extension MessageSender {
let recipientX25519PublicKey = Data(hex: recipientHexEncodedX25519PublicKey.removingIdPrefixIfNeeded())
let verificationData = plaintext + Data(userEd25519KeyPair.publicKey) + recipientX25519PublicKey
guard let signature = dependencies.sign.signature(message: Bytes(verificationData), secretKey: userEd25519KeyPair.secretKey) else {
throw MessageSenderError.signingFailed
}
guard
let signature = try? dependencies.crypto.perform(
.signature(message: Bytes(verificationData), secretKey: userEd25519KeyPair.secretKey)
)
else { throw MessageSenderError.signingFailed }
let plaintextWithMetadata = plaintext + Data(userEd25519KeyPair.publicKey) + Data(signature)
guard let ciphertext = dependencies.box.seal(message: Bytes(plaintextWithMetadata), recipientPublicKey: Bytes(recipientX25519PublicKey)) else {
throw MessageSenderError.encryptionFailed
}
guard
let ciphertext = try? dependencies.crypto.perform(
.seal(
message: Bytes(plaintextWithMetadata),
recipientPublicKey: Bytes(recipientX25519PublicKey)
)
)
else { throw MessageSenderError.encryptionFailed }
return Data(ciphertext)
}
@ -36,7 +43,7 @@ extension MessageSender {
plaintext: Data,
for recipientBlindedId: String,
openGroupPublicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> Data {
guard
SessionId.Prefix(from: recipientBlindedId) == .blinded15 ||
@ -45,32 +52,37 @@ extension MessageSender {
guard let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw MessageSenderError.noUserED25519KeyPair
}
guard let blindedKeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, genericHash: dependencies.genericHash) else {
throw MessageSenderError.signingFailed
}
guard
let blindedKeyPair = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: openGroupPublicKey, edKeyPair: userEd25519KeyPair, using: dependencies)
)
else { throw MessageSenderError.signingFailed }
let recipientBlindedPublicKey = Data(hex: recipientBlindedId.removingIdPrefixIfNeeded())
/// Step one: calculate the shared encryption key, sending from A to B
guard let enc_key: Bytes = dependencies.sodium.sharedBlindedEncryptionKey(
secretKey: userEd25519KeyPair.secretKey,
otherBlindedPublicKey: recipientBlindedPublicKey.bytes,
fromBlindedPublicKey: blindedKeyPair.publicKey,
toBlindedPublicKey: recipientBlindedPublicKey.bytes,
genericHash: dependencies.genericHash
) else {
throw MessageSenderError.signingFailed
}
guard
let enc_key: Bytes = try? dependencies.crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: userEd25519KeyPair.secretKey,
otherBlindedPublicKey: recipientBlindedPublicKey.bytes,
fromBlindedPublicKey: blindedKeyPair.publicKey,
toBlindedPublicKey: recipientBlindedPublicKey.bytes,
using: dependencies
)
),
let nonce: Bytes = try? dependencies.crypto.perform(.generateNonce24())
else { throw MessageSenderError.signingFailed }
/// Inner data: msg || A (i.e. the sender's ed25519 master pubkey, *not* kA blinded pubkey)
let innerBytes: Bytes = (plaintext.bytes + userEd25519KeyPair.publicKey)
/// Encrypt using xchacha20-poly1305
let nonce: Bytes = dependencies.nonceGenerator24.nonce()
guard let ciphertext = dependencies.aeadXChaCha20Poly1305Ietf.encrypt(message: innerBytes, secretKey: enc_key, nonce: nonce) else {
throw MessageSenderError.encryptionFailed
}
guard
let ciphertext = try? dependencies.crypto.perform(
.encryptAeadXChaCha20(message: innerBytes, secretKey: enc_key, nonce: nonce, using: dependencies)
)
else { throw MessageSenderError.encryptionFailed }
/// data = b'\x00' + ciphertext + nonce
return Data(Bytes(arrayLiteral: 0) + ciphertext + nonce)

View File

@ -140,10 +140,10 @@ public final class MessageSender {
namespace: SnodeAPI.Namespace?,
interactionId: Int64?,
isSyncMessage: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData {
// Common logic for all destinations
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let updatedMessage: Message = message
@ -199,7 +199,7 @@ public final class MessageSender {
userPublicKey: String,
messageSendTimestamp: Int64,
isSyncMessage: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> PreparedSendData {
message.sender = userPublicKey
message.recipient = {
@ -276,7 +276,7 @@ public final class MessageSender {
do {
switch destination {
case .contact(let publicKey):
ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: publicKey)
ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: publicKey, using: dependencies)
case .closedGroup(let groupPublicKey):
guard let encryptionKeyPair: ClosedGroupKeyPair = try? ClosedGroupKeyPair.fetchLatestKeyPair(db, threadId: groupPublicKey) else {
@ -286,7 +286,8 @@ public final class MessageSender {
ciphertext = try encryptWithSessionProtocol(
db,
plaintext: plaintext,
for: SessionId(.standard, publicKey: encryptionKeyPair.publicKey.bytes).hexString
for: SessionId(.standard, publicKey: encryptionKeyPair.publicKey.bytes).hexString,
using: dependencies
)
case .openGroup, .openGroupInbox: preconditionFailure()
@ -365,7 +366,7 @@ public final class MessageSender {
to destination: Message.Destination,
interactionId: Int64?,
messageSendTimestamp: Int64,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> PreparedSendData {
let threadId: String
@ -394,7 +395,7 @@ public final class MessageSender {
throw MessageSenderError.invalidMessage
}
message.sender = {
message.sender = try {
let capabilities: [Capability.Variant] = (try? Capability
.select(.variant)
.filter(Capability.Columns.openGroupServer == server)
@ -407,9 +408,11 @@ public final class MessageSender {
guard capabilities.isEmpty || capabilities.contains(.blind) else {
return SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString
}
guard let blindedKeyPair: KeyPair = dependencies.sodium.blindedKeyPair(serverPublicKey: openGroup.publicKey, edKeyPair: userEdKeyPair, genericHash: dependencies.genericHash) else {
preconditionFailure()
}
guard
let blindedKeyPair: KeyPair = dependencies.crypto.generate(
.blindedKeyPair(serverPublicKey: openGroup.publicKey, edKeyPair: userEdKeyPair, using: dependencies)
)
else { throw MessageSenderError.signingFailed }
return SessionId(.blinded15, publicKey: blindedKeyPair.publicKey).hexString
}()
@ -492,7 +495,7 @@ public final class MessageSender {
interactionId: Int64?,
userPublicKey: String,
messageSendTimestamp: Int64,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws -> PreparedSendData {
guard case .openGroupInbox(_, let openGroupPublicKey, let recipientBlindedPublicKey) = destination else {
throw MessageSenderError.invalidMessage
@ -582,10 +585,10 @@ public final class MessageSender {
// MARK: - Sending
public static func sendImmediate(
preparedSendData: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies()
data: PreparedSendData,
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard preparedSendData.shouldSend else {
guard data.shouldSend else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
@ -597,7 +600,7 @@ public final class MessageSender {
//
// If you see this error then you need to call
// `MessageSender.performUploadsIfNeeded(queue:preparedSendData:)` before calling this function
switch preparedSendData.message {
switch data.message {
case let visibleMessage as VisibleMessage:
let expectedAttachmentUploadCount: Int = (
visibleMessage.attachmentIds.count +
@ -605,17 +608,17 @@ public final class MessageSender {
(visibleMessage.quote?.attachmentId != nil ? 1 : 0)
)
guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else {
guard expectedAttachmentUploadCount == data.totalAttachmentsUploaded else {
// Make sure to actually handle this as a failure (if we don't then the message
// won't go into an error state correctly)
if let message: Message = preparedSendData.message {
if let message: Message = data.message {
dependencies.storage.read { db in
MessageSender.handleFailedMessageSend(
db,
message: message,
with: .attachmentsNotUploaded,
interactionId: preparedSendData.interactionId,
isSyncMessage: (preparedSendData.isSyncMessage == true),
interactionId: data.interactionId,
isSyncMessage: (data.isSyncMessage == true),
using: dependencies
)
}
@ -630,10 +633,10 @@ public final class MessageSender {
default: break
}
switch preparedSendData.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: preparedSendData, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: preparedSendData, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: preparedSendData, using: dependencies)
switch data.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: data, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: data, using: dependencies)
}
}
@ -641,7 +644,7 @@ public final class MessageSender {
private static func sendToSnodeDestination(
data: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard
let message: Message = data.message,
@ -653,14 +656,11 @@ public final class MessageSender {
.eraseToAnyPublisher()
}
return SnodeAPI
.sendMessage(
snodeMessage,
in: namespace
)
.flatMap { response -> AnyPublisher<Void, Error> in
return dependencies.network
.send(.message(snodeMessage, in: namespace, using: dependencies))
.flatMap { info, response -> AnyPublisher<Void, Error> in
let updatedMessage: Message = message
updatedMessage.serverHash = response.1.hash
updatedMessage.serverHash = response.hash
let job: Job? = Job(
variant: .notifyPushServer,
@ -695,7 +695,7 @@ public final class MessageSender {
guard shouldNotify else { return () }
JobRunner.add(db, job: job)
dependencies.jobRunner.add(db, job: job, canStartJob: true, using: dependencies)
return ()
}
.flatMap { _ -> AnyPublisher<Void, Error> in
@ -726,7 +726,8 @@ public final class MessageSender {
deferred: { _, _ in
// Always fulfill because the notify PN server job isn't critical.
resolver(Result.success(()))
}
},
using: dependencies
)
}
}
@ -761,7 +762,7 @@ public final class MessageSender {
private static func sendToOpenGroupDestination(
data: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard
let message: Message = data.message,
@ -829,7 +830,7 @@ public final class MessageSender {
private static func sendToOpenGroupInbox(
data: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Void, Error> {
guard
let message: Message = data.message,
@ -926,7 +927,7 @@ public final class MessageSender {
interactionId: Int64?,
serverTimestampMs: UInt64? = nil,
isSyncMessage: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) throws {
// If the message was a reaction then we want to update the reaction instead of the original
// interaction (which the 'interactionId' is pointing to
@ -964,13 +965,15 @@ public final class MessageSender {
.updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.sent))
// Start the disappearing messages timer if needed
JobRunner.upsert(
dependencies.jobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interaction: interaction,
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
)
),
canStartJob: true,
using: dependencies
)
}
}
@ -995,7 +998,8 @@ public final class MessageSender {
destination: destination,
threadId: threadId,
interactionId: interactionId,
isAlreadySyncMessage: isSyncMessage
isAlreadySyncMessage: isSyncMessage,
using: dependencies
)
}
@ -1005,7 +1009,7 @@ public final class MessageSender {
with error: MessageSenderError,
interactionId: Int64?,
isSyncMessage: Bool = false,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> Error {
// If the message was a reaction then we don't want to do anything to the original
// interaciton (which the 'interactionId' is pointing to
@ -1072,11 +1076,12 @@ public final class MessageSender {
destination: Message.Destination,
threadId: String?,
interactionId: Int64?,
isAlreadySyncMessage: Bool
isAlreadySyncMessage: Bool,
using dependencies: Dependencies
) {
// Sync the message if it's not a sync message, wasn't already sent to the current user and
// it's a message type which should be synced
let currentUserPublicKey = getUserHexEncodedPublicKey(db)
let currentUserPublicKey = getUserHexEncodedPublicKey(db, using: dependencies)
if
case .contact(let publicKey) = destination,
@ -1087,7 +1092,7 @@ public final class MessageSender {
if let message = message as? VisibleMessage { message.syncTarget = publicKey }
if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey }
JobRunner.add(
dependencies.jobRunner.add(
db,
job: Job(
variant: .messageSend,
@ -1098,7 +1103,9 @@ public final class MessageSender {
message: message,
isSyncMessage: true
)
)
),
canStartJob: true,
using: dependencies
)
}
}

View File

@ -23,23 +23,23 @@ public final class ClosedGroupPoller: Poller {
// MARK: - Public API
public func start() {
public func start(using dependencies: Dependencies = Dependencies()) {
// Fetch all closed groups (excluding any don't contain the current user as a
// GroupMemeber as the user is no longer a member of those)
Storage.shared
dependencies.storage
.read { db in
try ClosedGroup
.select(.threadId)
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db, using: dependencies))
)
.asRequest(of: String.self)
.fetchAll(db)
}
.defaulting(to: [])
.forEach { [weak self] publicKey in
self?.startIfNeeded(for: publicKey)
self?.startIfNeeded(for: publicKey, using: dependencies)
}
}
@ -49,7 +49,7 @@ public final class ClosedGroupPoller: Poller {
return "closed group with public key: \(publicKey)"
}
override func nextPollDelay(for publicKey: String) -> TimeInterval {
override func nextPollDelay(for publicKey: String, using dependencies: Dependencies) -> TimeInterval {
// Get the received date of the last message in the thread. If we don't have
// any messages yet, pick some reasonable fake time interval to use instead
let lastMessageDate: Date = Storage.shared
@ -68,7 +68,7 @@ public final class ClosedGroupPoller: Poller {
}
.defaulting(to: Date().addingTimeInterval(-5 * 60))
let timeSinceLastMessage: TimeInterval = Date().timeIntervalSince(lastMessageDate)
let timeSinceLastMessage: TimeInterval = dependencies.dateNow.timeIntervalSince(lastMessageDate)
let minPollInterval: Double = ClosedGroupPoller.minPollInterval
let limit: Double = (12 * 60 * 60)
let a: TimeInterval = ((ClosedGroupPoller.maxPollInterval - minPollInterval) / limit)
@ -78,11 +78,7 @@ public final class ClosedGroupPoller: Poller {
return nextPollInterval
}
override func handlePollError(
_ error: Error,
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
) -> Bool {
override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool {
SNLog("Polling failed for closed group with public key: \(publicKey) due to error: \(error).")
return true
}

View File

@ -33,13 +33,13 @@ public final class CurrentUserPoller: Poller {
// MARK: - Convenience Functions
public func start() {
let publicKey: String = getUserHexEncodedPublicKey()
public func start(using dependencies: Dependencies = Dependencies()) {
let publicKey: String = getUserHexEncodedPublicKey(using: dependencies)
guard isPolling.wrappedValue[publicKey] != true else { return }
SNLog("Started polling.")
super.startIfNeeded(for: publicKey)
super.startIfNeeded(for: publicKey, using: dependencies)
}
public func stop() {
@ -53,7 +53,7 @@ public final class CurrentUserPoller: Poller {
return "Main Poller"
}
override func nextPollDelay(for publicKey: String) -> TimeInterval {
override func nextPollDelay(for publicKey: String, using dependencies: Dependencies) -> TimeInterval {
let failureCount: TimeInterval = TimeInterval(failureCount.wrappedValue[publicKey] ?? 0)
// If there have been no failures then just use the 'minPollInterval'
@ -65,11 +65,7 @@ public final class CurrentUserPoller: Poller {
return min(maxRetryInterval, nextDelay)
}
override func handlePollError(
_ error: Error,
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
) -> Bool {
override func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool {
if UserDefaults.sharedLokiProject?[.isMainAppActive] != true {
// Do nothing when an error gets throws right after returning from the background (happens frequently)
}

View File

@ -35,7 +35,7 @@ extension OpenGroupAPI {
self.server = server
}
public func startIfNeeded(using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()) {
public func startIfNeeded(using dependencies: Dependencies) {
guard !hasStarted else { return }
hasStarted = true
@ -49,20 +49,15 @@ extension OpenGroupAPI {
// MARK: - Polling
private func pollRecursively(
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies(
subscribeQueue: Threading.pollerQueue,
receiveQueue: OpenGroupAPI.workQueue
)
) {
private func pollRecursively(using dependencies: Dependencies) {
guard hasStarted else { return }
let server: String = self.server
let lastPollStart: TimeInterval = Date().timeIntervalSince1970
let lastPollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
poll(using: dependencies)
.subscribe(on: dependencies.subscribeQueue)
.receive(on: dependencies.receiveQueue)
.subscribe(on: Threading.pollerQueue, using: dependencies)
.receive(on: OpenGroupAPI.workQueue, using: dependencies)
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
let minPollFailureCount: Int64 = dependencies.storage
@ -76,7 +71,7 @@ extension OpenGroupAPI {
.defaulting(to: 0)
// Calculate the remaining poll delay
let currentTime: TimeInterval = Date().timeIntervalSince1970
let currentTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
let nextPollInterval: TimeInterval = Poller.getInterval(
for: TimeInterval(minPollFailureCount),
minInterval: Poller.minPollInterval,
@ -86,12 +81,12 @@ extension OpenGroupAPI {
// Schedule the next poll
guard remainingInterval > 0 else {
return dependencies.subscribeQueue.async {
return Threading.pollerQueue.async(using: dependencies) {
self?.pollRecursively(using: dependencies)
}
}
dependencies.subscribeQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default) {
Threading.pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default, using: dependencies) {
self?.pollRecursively(using: dependencies)
}
}
@ -99,7 +94,7 @@ extension OpenGroupAPI {
}
public func poll(
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return poll(
calledFromBackgroundPoller: false,
@ -112,7 +107,7 @@ extension OpenGroupAPI {
calledFromBackgroundPoller: Bool,
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
isPostCapabilitiesRetry: Bool,
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard !self.isPolling else {
return Just(())
@ -122,10 +117,12 @@ extension OpenGroupAPI {
self.isPolling = true
let server: String = self.server
let hasPerformedInitialPoll: Bool = (dependencies.cache.hasPerformedInitialPoll[server] == true)
let hasPerformedInitialPoll: Bool = (dependencies.caches[.openGroupManager].hasPerformedInitialPoll[server] == true)
let timeSinceLastPoll: TimeInterval = (
dependencies.cache.timeSinceLastPoll[server] ??
dependencies.mutableCache.mutate { $0.getTimeSinceLastOpen(using: dependencies) }
dependencies.caches[.openGroupManager].timeSinceLastPoll[server] ??
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.getTimeSinceLastOpen(using: dependencies)
}
)
return dependencies.storage
@ -170,10 +167,11 @@ extension OpenGroupAPI {
using: dependencies
)
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.hasPerformedInitialPoll[server] = true
cache.timeSinceLastPoll[server] = Date().timeIntervalSince1970
UserDefaults.standard[.lastOpen] = Date()
cache.timeSinceLastPoll[server] = dependencies.dateNow.timeIntervalSince1970
dependencies.standardUserDefaults[.lastOpen] = dependencies.dateNow
}
SNLog("Open group polling finished for \(server).")
@ -303,7 +301,7 @@ extension OpenGroupAPI {
isBackgroundPollerValid: @escaping (() -> Bool) = { true },
isPostCapabilitiesRetry: Bool,
error: Error,
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Bool, Error> {
/// We want to custom handle a '400' error code due to not having blinded auth as it likely means that we join the
/// OpenGroup before blinding was enabled and need to update it's capabilities
@ -376,7 +374,7 @@ extension OpenGroupAPI {
info: ResponseInfoType,
response: BatchResponse,
failureCount: Int64,
using dependencies: OpenGroupManager.OGMDependencies = OpenGroupManager.OGMDependencies()
using dependencies: Dependencies
) {
let server: String = self.server
let validResponses: [OpenGroupAPI.Endpoint: Decodable] = response.data
@ -550,7 +548,7 @@ extension OpenGroupAPI {
publicKey: nil,
for: roomToken,
on: server,
dependencies: dependencies
using: dependencies
)
case .roomMessagesRecent(let roomToken), .roomMessagesBefore(let roomToken, _), .roomMessagesSince(let roomToken, _):
@ -564,7 +562,7 @@ extension OpenGroupAPI {
messages: responseBody.compactMap { $0.value },
for: roomToken,
on: server,
dependencies: dependencies
using: dependencies
)
case .inbox, .inboxSince, .outbox, .outboxSince:
@ -587,7 +585,7 @@ extension OpenGroupAPI {
messages: messages,
fromOutbox: fromOutbox,
on: server,
dependencies: dependencies
using: dependencies
)
default: break // No custom handling needed

View File

@ -53,18 +53,18 @@ public class Poller {
}
/// Calculate the delay which should occur before the next poll
internal func nextPollDelay(for publicKey: String) -> TimeInterval {
internal func nextPollDelay(for publicKey: String, using dependencies: Dependencies) -> TimeInterval {
preconditionFailure("abstract class - override in subclass")
}
/// Perform and logic which should occur when the poll errors, will stop polling if `false` is returned
internal func handlePollError(_ error: Error, for publicKey: String, using dependencies: SMKDependencies) -> Bool {
internal func handlePollError(_ error: Error, for publicKey: String, using dependencies: Dependencies) -> Bool {
preconditionFailure("abstract class - override in subclass")
}
// MARK: - Private API
internal func startIfNeeded(for publicKey: String) {
internal func startIfNeeded(for publicKey: String, using dependencies: Dependencies) {
// Run on the 'pollerQueue' to ensure any 'Atomic' access doesn't block the main thread
// on startup
Threading.pollerQueue.async { [weak self] in
@ -74,13 +74,13 @@ public class Poller {
// and the timer is not created, if we mark the group as is polling
// after setUpPolling. So the poller may not work, thus misses messages
self?.isPolling.mutate { $0[publicKey] = true }
self?.pollRecursively(for: publicKey)
self?.pollRecursively(for: publicKey, using: dependencies)
}
}
internal func getSnodeForPolling(
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Snode, Error> {
// If we don't want to poll a snode multiple times then just grab a random one from the swarm
guard maxNodePollCount > 0 else {
@ -135,14 +135,14 @@ public class Poller {
private func pollRecursively(
for publicKey: String,
using dependencies: SMKDependencies = SMKDependencies()
using dependencies: Dependencies
) {
guard isPolling.wrappedValue[publicKey] == true else { return }
let namespaces: [SnodeAPI.Namespace] = self.namespaces
let lastPollStart: TimeInterval = Date().timeIntervalSince1970
let lastPollInterval: TimeInterval = nextPollDelay(for: publicKey)
let getSnodePublisher: AnyPublisher<Snode, Error> = getSnodeForPolling(for: publicKey)
let lastPollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
let lastPollInterval: TimeInterval = nextPollDelay(for: publicKey, using: dependencies)
let getSnodePublisher: AnyPublisher<Snode, Error> = getSnodeForPolling(for: publicKey, using: dependencies)
// Store the publisher intp the cancellables dictionary
cancellables.mutate { [weak self] cancellables in
@ -156,8 +156,8 @@ public class Poller {
using: dependencies
)
}
.subscribe(on: dependencies.subscribeQueue)
.receive(on: dependencies.receiveQueue)
.subscribe(on: Threading.pollerQueue, using: dependencies)
.receive(on: Threading.pollerQueue, using: dependencies)
.sink(
receiveCompletion: { result in
switch result {
@ -174,21 +174,21 @@ public class Poller {
self?.incrementPollCount(publicKey: publicKey)
// Calculate the remaining poll delay
let currentTime: TimeInterval = Date().timeIntervalSince1970
let currentTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970
let nextPollInterval: TimeInterval = (
self?.nextPollDelay(for: publicKey) ??
self?.nextPollDelay(for: publicKey, using: dependencies) ??
lastPollInterval
)
let remainingInterval: TimeInterval = max(0, nextPollInterval - (currentTime - lastPollStart))
// Schedule the next poll
guard remainingInterval > 0 else {
return dependencies.subscribeQueue.async {
return Threading.pollerQueue.async(using: dependencies) {
self?.pollRecursively(for: publicKey, using: dependencies)
}
}
dependencies.subscribeQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default) {
Threading.pollerQueue.asyncAfter(deadline: .now() + .milliseconds(Int(remainingInterval * 1000)), qos: .default, using: dependencies) {
self?.pollRecursively(for: publicKey, using: dependencies)
}
},
@ -209,10 +209,7 @@ public class Poller {
calledFromBackgroundPoller: Bool = false,
isBackgroundPollValid: @escaping (() -> Bool) = { true },
poller: Poller? = nil,
using dependencies: SMKDependencies = SMKDependencies(
subscribeQueue: Threading.pollerQueue,
receiveQueue: Threading.pollerQueue
)
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[Message], Error> {
// If the polling has been cancelled then don't continue
guard
@ -276,7 +273,7 @@ public class Poller {
var standardMessageJobsToRun: [Job] = []
var pollerLogOutput: String = "\(pollerName) failed to process any messages"
Storage.shared.write { db in
dependencies.storage.write { db in
let allProcessedMessages: [ProcessedMessage] = allMessages
.compactMap { message -> ProcessedMessage? in
do {
@ -338,7 +335,7 @@ public class Poller {
db,
job: jobToRun,
canStartJob: !calledFromBackgroundPoller,
dependencies: dependencies
using: dependencies
)
return updatedJob?.id
@ -372,7 +369,7 @@ public class Poller {
db,
job: jobToRun,
canStartJob: !calledFromBackgroundPoller,
dependencies: dependencies
using: dependencies
)
// Create the dependency between the jobs
@ -429,10 +426,11 @@ public class Poller {
// Note: In the background we just want jobs to fail silently
ConfigMessageReceiveJob.run(
job,
queue: dependencies.receiveQueue,
queue: Threading.pollerQueue,
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ in resolver(Result.success(())) },
deferred: { _, _ in resolver(Result.success(())) }
deferred: { _, _ in resolver(Result.success(())) },
using: dependencies
)
}
}
@ -449,10 +447,11 @@ public class Poller {
// Note: In the background we just want jobs to fail silently
MessageReceiveJob.run(
job,
queue: dependencies.receiveQueue,
queue: Threading.pollerQueue,
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ in resolver(Result.success(())) },
deferred: { _, _ in resolver(Result.success(())) }
deferred: { _, _ in resolver(Result.success(())) },
using: dependencies
)
}
}

View File

@ -54,11 +54,11 @@ public class TypingIndicators {
self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs())
}
fileprivate func start(_ db: Database) {
fileprivate func start(_ db: Database, using dependencies: Dependencies = Dependencies()) {
// Start the typing indicator
switch direction {
case .outgoing:
scheduleRefreshCallback(db, shouldSend: (refreshTimer == nil))
scheduleRefreshCallback(db, shouldSend: (refreshTimer == nil), using: dependencies)
case .incoming:
try? ThreadTypingIndicator(
@ -72,7 +72,7 @@ public class TypingIndicators {
refreshTimeout()
}
fileprivate func stop(_ db: Database) {
fileprivate func stop(_ db: Database, using dependencies: Dependencies = Dependencies()) {
self.refreshTimer?.invalidate()
self.refreshTimer = nil
self.stopTimer?.invalidate()
@ -85,7 +85,8 @@ public class TypingIndicators {
message: TypingIndicator(kind: .stopped),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
case .incoming:
@ -111,14 +112,19 @@ public class TypingIndicators {
}
}
private func scheduleRefreshCallback(_ db: Database, shouldSend: Bool = true) {
private func scheduleRefreshCallback(
_ db: Database,
shouldSend: Bool = true,
using dependencies: Dependencies
) {
if shouldSend {
try? MessageSender.send(
db,
message: TypingIndicator(kind: .started),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
@ -127,8 +133,8 @@ public class TypingIndicators {
withTimeInterval: 10,
repeats: false
) { [weak self] _ in
Storage.shared.writeAsync { db in
self?.scheduleRefreshCallback(db)
dependencies.storage.writeAsync { db in
self?.scheduleRefreshCallback(db, using: dependencies)
}
}
}

View File

@ -29,7 +29,8 @@ internal extension SessionUtil {
_ db: Database,
in conf: UnsafeMutablePointer<config_object>?,
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
latestConfigSentTimestampMs: Int64,
using dependencies: Dependencies = Dependencies()
) throws {
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
@ -236,7 +237,8 @@ internal extension SessionUtil {
admins: updatedAdmins.map { $0.profileId },
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
formationTimestampMs: UInt64((group.joinedAt.map { $0 * 1000 } ?? latestConfigSentTimestampMs)),
calledFromConfigHandling: true
calledFromConfigHandling: true,
using: dependencies
)
}
else {

View File

@ -18,7 +18,8 @@ internal extension SessionUtil {
_ db: Database,
in conf: UnsafeMutablePointer<config_object>?,
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
latestConfigSentTimestampMs: Int64,
using dependencies: Dependencies = Dependencies()
) throws {
typealias ProfileData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?)
@ -51,7 +52,8 @@ internal extension SessionUtil {
)
}(),
sentTimestamp: (TimeInterval(latestConfigSentTimestampMs) / 1000),
calledFromConfigHandling: true
calledFromConfigHandling: true,
using: dependencies
)
// Update the 'Note to Self' visibility and priority

View File

@ -0,0 +1,144 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import Clibsodium
import Curve25519Kit
import SessionUtilitiesKit
// MARK: - Generic Hash
public extension Crypto.Action {
static func hash(message: Bytes, key: Bytes?) -> Crypto.Action {
return Crypto.Action(id: "hash", args: [message, key]) {
Sodium().genericHash.hash(message: message, key: key)
}
}
static func hash(message: Bytes, outputLength: Int) -> Crypto.Action {
return Crypto.Action(id: "hashOutputLength", args: [message, outputLength]) {
Sodium().genericHash.hash(message: message, outputLength: outputLength)
}
}
static func hashSaltPersonal(
message: Bytes,
outputLength: Int,
key: Bytes? = nil,
salt: Bytes,
personal: Bytes
) -> Crypto.Action {
return Crypto.Action(
id: "hashSaltPersonal",
args: [message, outputLength, key, salt, personal]
) {
var output: [UInt8] = [UInt8](repeating: 0, count: outputLength)
let result = crypto_generichash_blake2b_salt_personal(
&output,
outputLength,
message,
UInt64(message.count),
key,
(key?.count ?? 0),
salt,
personal
)
guard result == 0 else { return nil }
return output
}
}
}
// MARK: - Sign
public extension Crypto.Action {
static func toX25519(ed25519PublicKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519PublicKey]) {
Sodium().sign.toX25519(ed25519PublicKey: ed25519PublicKey)
}
}
static func toX25519(ed25519SecretKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "toX25519", args: [ed25519SecretKey]) {
Sodium().sign.toX25519(ed25519SecretKey: ed25519SecretKey)
}
}
static func signature(message: Bytes, secretKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "signature", args: [message, secretKey]) {
Sodium().sign.signature(message: message, secretKey: secretKey)
}
}
}
public extension Crypto.Verification {
static func signature(message: Bytes, publicKey: Bytes, signature: Bytes) -> Crypto.Verification {
return Crypto.Verification(id: "signature", args: [message, publicKey, signature]) {
Sodium().sign.verify(message: message, publicKey: publicKey, signature: signature)
}
}
}
// MARK: - Box
public extension Crypto.Size {
static let signature: Crypto.Size = Crypto.Size(id: "signature") { Sodium().sign.Bytes }
static let publicKey: Crypto.Size = Crypto.Size(id: "publicKey") { Sodium().sign.PublicKeyBytes }
}
public extension Crypto.Action {
static func seal(message: Bytes, recipientPublicKey: Bytes) -> Crypto.Action {
return Crypto.Action(id: "seal", args: [message, recipientPublicKey]) {
Sodium().box.seal(message: message, recipientPublicKey: recipientPublicKey)
}
}
static func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Crypto.Action {
return Crypto.Action(
id: "open",
args: [anonymousCipherText, recipientPublicKey, recipientSecretKey]
) {
Sodium().box.open(
anonymousCipherText: anonymousCipherText,
recipientPublicKey: recipientPublicKey,
recipientSecretKey: recipientSecretKey
)
}
}
}
// MARK: - Ed25519
public extension Crypto.Action {
static func signEd25519(data: Bytes, keyPair: KeyPair) -> Crypto.Action {
return Crypto.Action(id: "signEd25519", args: [data, keyPair]) {
let ecKeyPair: ECKeyPair = try ECKeyPair(
publicKeyData: Data(keyPair.publicKey),
privateKeyData: Data(keyPair.secretKey)
)
return try Ed25519.sign(Data(data), with: ecKeyPair).bytes
}
}
}
public extension Crypto.Verification {
static func signatureEd25519(_ signature: Data, publicKey: Data, data: Data) -> Crypto.Verification {
return Crypto.Verification(id: "signatureEd25519", args: [signature, publicKey, data]) {
return ((try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true)
}
}
}
public extension Crypto.KeyPairType {
static func x25519KeyPair() -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "x25519KeyPair") {
let keyPair: ECKeyPair = Curve25519.generateKeyPair()
return KeyPair(publicKey: Array(keyPair.publicKey), secretKey: Array(keyPair.privateKey))
}
}
}

View File

@ -286,9 +286,10 @@ public struct ProfileManager {
profileName: String,
avatarUpdate: AvatarUpdate = .none,
success: ((Database) throws -> ())? = nil,
failure: ((ProfileManagerError) -> ())? = nil
failure: ((ProfileManagerError) -> ())? = nil,
using dependencies: Dependencies = Dependencies()
) {
let userPublicKey: String = getUserHexEncodedPublicKey()
let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
let isRemovingAvatar: Bool = {
switch avatarUpdate {
case .remove: return true
@ -298,7 +299,7 @@ public struct ProfileManager {
switch avatarUpdate {
case .none, .remove, .updateTo:
Storage.shared.writeAsync { db in
dependencies.storage.writeAsync { db in
if isRemovingAvatar {
let existingProfileUrl: String? = try Profile
.filter(id: userPublicKey)
@ -327,7 +328,8 @@ public struct ProfileManager {
publicKey: userPublicKey,
name: profileName,
avatarUpdate: avatarUpdate,
sentTimestamp: Date().timeIntervalSince1970
sentTimestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies
)
SNLog("Successfully updated service with profile.")
@ -345,7 +347,8 @@ public struct ProfileManager {
publicKey: userPublicKey,
name: profileName,
avatarUpdate: .updateTo(url: downloadUrl, key: newProfileKey, fileName: fileName),
sentTimestamp: Date().timeIntervalSince1970
sentTimestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies
)
SNLog("Successfully updated service with profile.")
@ -498,9 +501,9 @@ public struct ProfileManager {
avatarUpdate: AvatarUpdate,
sentTimestamp: TimeInterval,
calledFromConfigHandling: Bool = false,
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) throws {
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db, dependencies: dependencies))
let isCurrentUser = (publicKey == getUserHexEncodedPublicKey(db, using: dependencies))
let profile: Profile = Profile.fetchOrCreate(db, id: publicKey)
var profileChanges: [ConfigColumnAssignment] = []
@ -604,7 +607,7 @@ public struct ProfileManager {
let targetProfile: Profile = Profile.fetchOrCreate(db, id: publicKey)
// FIXME: Refactor avatar downloading to be a proper Job so we can avoid this
JobRunner.afterBlockingQueue {
dependencies.jobRunner.afterBlockingQueue {
ProfileManager.downloadAvatar(for: targetProfile)
}
}

View File

@ -1,285 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import CryptoKit
import Clibsodium
import Sodium
import Curve25519Kit
import SessionUtilitiesKit
/// These extenion methods are used to generate a sign "blinded" messages
///
/// According to the Swift engineers the only situation when `UnsafeRawBufferPointer.baseAddress` is nil is when it's an
/// empty collection; as such our guard cases wihch return `-1` when unwrapping this value should never be hit and we can ignore
/// them as possible results.
///
/// For more information see:
/// https://forums.swift.org/t/when-is-unsafemutablebufferpointer-baseaddress-nil/32136/5
/// https://github.com/apple/swift-evolution/blob/master/proposals/0055-optional-unsafe-pointers.md#unsafebufferpointer
extension Sodium {
private static let scalarLength: Int = Int(crypto_core_ed25519_scalarbytes()) // 32
private static let noClampLength: Int = Int(Sodium.lib_crypto_scalarmult_ed25519_bytes()) // 32
private static let scalarMultLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let publicKeyLength: Int = Int(crypto_scalarmult_bytes()) // 32
private static let secretKeyLength: Int = Int(crypto_sign_secretkeybytes()) // 64
/// 64-byte blake2b hash then reduce to get the blinding factor
public func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
/// k = salt.crypto_core_ed25519_scalar_reduce(blake2b(server_pk, digest_size=64).digest())
let serverPubKeyData: Data = Data(hex: serverPublicKey)
guard !serverPubKeyData.isEmpty, let serverPublicKeyHashBytes: Bytes = genericHash.hash(message: [UInt8](serverPubKeyData), outputLength: 64) else {
return nil
}
/// Reduce the server public key into an ed25519 scalar (`k`)
let kPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
_ = serverPublicKeyHashBytes.withUnsafeBytes { (serverPublicKeyHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let serverPublicKeyHashBaseAddress: UnsafePointer<UInt8> = serverPublicKeyHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(kPtr, serverPublicKeyHashBaseAddress)
return 0
}
return Data(bytes: kPtr, count: Sodium.scalarLength).bytes
}
/// Calculate k*a. To get 'a' (the Ed25519 private key scalar) we call the sodium function to
/// convert to an *x* secret key, which seems wrong--but isn't because converted keys use the
/// same secret scalar secret (and so this is just the most convenient way to get 'a' out of
/// a sodium Ed25519 secret key)
func generatePrivateKeyScalar(secretKey: Bytes) -> Bytes {
/// a = s.to_curve25519_private_key().encode()
let aPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarMultLength)
/// Looks like the `crypto_sign_ed25519_sk_to_curve25519` function can't actually fail so no need to verify the result
/// See: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_sign/ed25519/ref10/keypair.c#L70
_ = secretKey.withUnsafeBytes { (secretKeyPtr: UnsafeRawBufferPointer) -> Int32 in
guard let secretKeyBaseAddress: UnsafePointer<UInt8> = secretKeyPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
return crypto_sign_ed25519_sk_to_curve25519(aPtr, secretKeyBaseAddress)
}
return Data(bytes: aPtr, count: Sodium.scalarMultLength).bytes
}
/// Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair`
public func blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair, genericHash: GenericHashType) -> KeyPair? {
guard edKeyPair.publicKey.count == Sodium.publicKeyLength && edKeyPair.secretKey.count == Sodium.secretKeyLength else {
return nil
}
guard let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash) else {
return nil
}
let aBytes: Bytes = generatePrivateKeyScalar(secretKey: edKeyPair.secretKey)
/// Generate the blinded key pair `ka`, `kA`
let kaPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.secretKeyLength)
let kAPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.publicKeyLength)
_ = aBytes.withUnsafeBytes { (aPtr: UnsafeRawBufferPointer) -> Int32 in
return kBytes.withUnsafeBytes { (kPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kBaseAddress: UnsafePointer<UInt8> = kPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
guard let aBaseAddress: UnsafePointer<UInt8> = aPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_mul(kaPtr, kBaseAddress, aBaseAddress)
return 0
}
}
guard crypto_scalarmult_ed25519_base_noclamp(kAPtr, kaPtr) == 0 else { return nil }
return KeyPair(
publicKey: Data(bytes: kAPtr, count: Sodium.publicKeyLength).bytes,
secretKey: Data(bytes: kaPtr, count: Sodium.secretKeyLength).bytes
)
}
/// Constructs an Ed25519 signature from a root Ed25519 key and a blinded scalar/pubkey pair, with one tweak to the
/// construction: we add kA into the hashed value that yields r so that we have domain separation for different blinded
/// pubkeys (this doesn't affect verification at all)
public func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
/// H_rh = sha512(s.encode()).digest()[32:]
let H_rh: Bytes = Bytes(SHA512.hash(data: secretKey).suffix(32))
/// r = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(H_rh, kA, message_parts))
let combinedHashBytes: Bytes = SHA512.hash(data: H_rh + kA + message).bytes
let rPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
_ = combinedHashBytes.withUnsafeBytes { (combinedHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let combinedHashBaseAddress: UnsafePointer<UInt8> = combinedHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(rPtr, combinedHashBaseAddress)
return 0
}
/// sig_R = salt.crypto_scalarmult_ed25519_base_noclamp(r)
let sig_RPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
guard crypto_scalarmult_ed25519_base_noclamp(sig_RPtr, rPtr) == 0 else { return nil }
/// HRAM = salt.crypto_core_ed25519_scalar_reduce(sha512_multipart(sig_R, kA, message_parts))
let sig_RBytes: Bytes = Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes
let HRAMHashBytes: Bytes = SHA512.hash(data: sig_RBytes + kA + message).bytes
let HRAMPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
_ = HRAMHashBytes.withUnsafeBytes { (HRAMHashPtr: UnsafeRawBufferPointer) -> Int32 in
guard let HRAMHashBaseAddress: UnsafePointer<UInt8> = HRAMHashPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_reduce(HRAMPtr, HRAMHashBaseAddress)
return 0
}
/// sig_s = salt.crypto_core_ed25519_scalar_add(r, salt.crypto_core_ed25519_scalar_mul(HRAM, ka))
let sig_sMulPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
let sig_sPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.scalarLength)
_ = ka.withUnsafeBytes { (kaPtr: UnsafeRawBufferPointer) -> Int32 in
guard let kaBaseAddress: UnsafePointer<UInt8> = kaPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
Sodium.lib_crypto_core_ed25519_scalar_mul(sig_sMulPtr, HRAMPtr, kaBaseAddress)
Sodium.lib_crypto_core_ed25519_scalar_add(sig_sPtr, rPtr, sig_sMulPtr)
return 0
}
/// full_sig = sig_R + sig_s
return (Data(bytes: sig_RPtr, count: Sodium.noClampLength).bytes + Data(bytes: sig_sPtr, count: Sodium.scalarLength).bytes)
}
/// Combines two keys (`kA`)
public func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? {
let combinedPtr: UnsafeMutablePointer<UInt8> = UnsafeMutablePointer<UInt8>.allocate(capacity: Sodium.noClampLength)
let result = rhsKeyBytes.withUnsafeBytes { (rhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
return lhsKeyBytes.withUnsafeBytes { (lhsKeyBytesPtr: UnsafeRawBufferPointer) -> Int32 in
guard let lhsKeyBytesBaseAddress: UnsafePointer<UInt8> = lhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
guard let rhsKeyBytesBaseAddress: UnsafePointer<UInt8> = rhsKeyBytesPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
return -1 // Impossible case (refer to comments at top of extension)
}
return Sodium.lib_crypto_scalarmult_ed25519_noclamp(combinedPtr, lhsKeyBytesBaseAddress, rhsKeyBytesBaseAddress)
}
}
/// Ensure the above worked
guard result == 0 else { return nil }
return Data(bytes: combinedPtr, count: Sodium.noClampLength).bytes
}
/// Calculate a shared secret for a message from A to B:
///
/// BLAKE2b(a kB || kA || kB)
///
/// The receiver can calulate the same value via:
///
/// BLAKE2b(b kA || kA || kB)
public func sharedBlindedEncryptionKey(secretKey: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? {
let aBytes: Bytes = generatePrivateKeyScalar(secretKey: secretKey)
guard let combinedKeyBytes: Bytes = combineKeys(lhsKeyBytes: aBytes, rhsKeyBytes: otherBlindedPublicKey) else {
return nil
}
return genericHash.hash(message: (combinedKeyBytes + kA + kB), outputLength: 32)
}
/// This method should be used to check if a users standard sessionId matches a blinded one
public func sessionId(_ standardSessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool {
// Only support generating blinded keys for standard session ids
guard
let sessionId: SessionId = SessionId(from: standardSessionId),
sessionId.prefix == .standard,
let blindedId: SessionId = SessionId(from: blindedSessionId),
(
blindedId.prefix == .blinded15 ||
blindedId.prefix == .blinded25
),
let kBytes: Bytes = generateBlindingFactor(serverPublicKey: serverPublicKey, genericHash: genericHash)
else { return false }
/// From the session id (ignoring 05 prefix) we have two possible ed25519 pubkeys; the first is the positive (which is what
/// Signal's XEd25519 conversion always uses)
///
/// Note: The below method is code we have exposed from the `curve25519_verify` method within the Curve25519 library
/// rather than custom code we have written
guard let xEd25519Key: Data = try? Ed25519.publicKey(from: Data(hex: sessionId.publicKey)) else { return false }
/// Blind the positive public key
guard let pk1: Bytes = combineKeys(lhsKeyBytes: kBytes, rhsKeyBytes: xEd25519Key.bytes) else { return false }
/// For the negative, what we're going to get out of the above is simply the negative of pk1, so flip the sign bit to get pk2
/// pk2 = pk1[0:31] + bytes([pk1[31] ^ 0b1000_0000])
let pk2: Bytes = (pk1[0..<31] + [(pk1[31] ^ 0b1000_0000)])
return (
SessionId(.blinded15, publicKey: pk1).publicKey == blindedId.publicKey ||
SessionId(.blinded15, publicKey: pk2).publicKey == blindedId.publicKey
)
}
}
extension GenericHash {
public func hashSaltPersonal(
message: Bytes,
outputLength: Int,
key: Bytes? = nil,
salt: Bytes,
personal: Bytes
) -> Bytes? {
var output: [UInt8] = [UInt8](repeating: 0, count: outputLength)
let result = crypto_generichash_blake2b_salt_personal(
&output,
outputLength,
message,
UInt64(message.count),
key,
(key?.count ?? 0),
salt,
personal
)
guard result == 0 else { return nil }
return output
}
}
extension AeadXChaCha20Poly1305IetfType {
/// This method is the same as the standard AeadXChaCha20Poly1305IetfType `encrypt` method except it allows the
/// specification of a nonce which allows for deterministic behaviour with unit testing
public func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes? = nil) -> Bytes? {
guard secretKey.count == KeyBytes else { return nil }
var authenticatedCipherText = Bytes(repeating: 0, count: message.count + ABytes)
var authenticatedCipherTextLen: UInt64 = 0
let result = crypto_aead_xchacha20poly1305_ietf_encrypt(
&authenticatedCipherText, &authenticatedCipherTextLen,
message, UInt64(message.count),
additionalData, UInt64(additionalData?.count ?? 0),
nil, nonce, secretKey
)
guard result == 0 else { return nil }
return authenticatedCipherText
}
}

View File

@ -15,8 +15,8 @@ class MessageSendJobSpec: QuickSpec {
override func spec() {
var job: Job!
var interaction: Interaction!
var attachment1: Attachment!
var interactionAttachment1: InteractionAttachment!
var attachment: Attachment!
var interactionAttachment: InteractionAttachment!
var mockStorage: Storage!
var mockJobRunner: MockJobRunner!
var dependencies: Dependencies!
@ -24,8 +24,10 @@ class MessageSendJobSpec: QuickSpec {
// MARK: - JobRunner
describe("a MessageSendJob") {
// MARK: - Configuration
beforeEach {
mockStorage = Storage(
mockStorage = SynchronousStorage(
customWriter: try! DatabaseQueue(),
customMigrationTargets: [
SNUtilitiesKit.self,
@ -36,9 +38,9 @@ class MessageSendJobSpec: QuickSpec {
dependencies = Dependencies(
storage: mockStorage,
jobRunner: mockJobRunner,
date: Date(timeIntervalSince1970: 1234567890)
dateNow: Date(timeIntervalSince1970: 1234567890)
)
attachment1 = Attachment(
attachment = Attachment(
id: "200",
variant: .standard,
state: .failedDownload,
@ -60,7 +62,7 @@ class MessageSendJobSpec: QuickSpec {
}
.thenReturn([:])
mockJobRunner
.when { $0.insert(any(), job: any(), before: any(), dependencies: dependencies) }
.when { $0.insert(any(), job: any(), before: any()) }
.then { args in
let db: Database = args[0] as! Database
var job: Job = args[1] as! Job
@ -77,6 +79,7 @@ class MessageSendJobSpec: QuickSpec {
dependencies = nil
}
// MARK: - fails when not given any details
it("fails when not given any details") {
job = Job(variant: .messageSend)
@ -92,14 +95,15 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
it("fails when not given incorrect details") {
// MARK: - fails when given incorrect details
it("fails when given incorrect details") {
job = Job(
variant: .messageSend,
details: MessageReceiveJob.Details(messages: [], calledFromBackgroundPoller: false)
@ -117,13 +121,14 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
// MARK: - of VisibleMessage
context("of VisibleMessage") {
beforeEach {
interaction = Interaction(
@ -162,6 +167,7 @@ class MessageSendJobSpec: QuickSpec {
}
}
// MARK: -- fails when there is no job id
it("fails when there is no job id") {
job = Job(
variant: .messageSend,
@ -186,13 +192,14 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
// MARK: -- fails when there is no interaction id
it("fails when there is no interaction id") {
job = Job(
variant: .messageSend,
@ -216,13 +223,14 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
// MARK: -- fails when there is no interaction for the provided interaction id
it("fails when there is no interaction for the provided interaction id") {
job = Job(
variant: .messageSend,
@ -248,29 +256,32 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(StorageError.objectNotFound))
expect(permanentFailure).to(beTrue())
}
// MARK: -- with an attachment
context("with an attachment") {
beforeEach {
interactionAttachment1 = InteractionAttachment(
interactionAttachment = InteractionAttachment(
albumIndex: 0,
interactionId: interaction.id!,
attachmentId: attachment1.id
attachmentId: attachment.id
)
mockStorage.write { db in
try attachment1.insert(db)
try interactionAttachment1.insert(db)
try attachment.insert(db)
try interactionAttachment.insert(db)
}
}
// MARK: ---- it fails when trying to send with an attachment which previously failed to download
it("it fails when trying to send with an attachment which previously failed to download") {
mockStorage.write { db in
try attachment1.with(state: .failedDownload).save(db)
try attachment.with(state: .failedDownload).save(db)
}
var error: Error? = nil
@ -285,54 +296,27 @@ class MessageSendJobSpec: QuickSpec {
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
)
expect(error).to(matchError(AttachmentError.notUploaded))
expect(permanentFailure).to(beTrue())
}
it("it fails when trying to send with an attachment that has an invalid downloadUrl") {
mockStorage.write { db in
try attachment1
.with(
state: .uploaded,
downloadUrl: nil
)
.save(db)
}
var error: Error? = nil
var permanentFailure: Bool = false
MessageSendJob.run(
job,
queue: .main,
success: { _, _, _ in },
failure: { _, runError, runPermanentFailure, _ in
error = runError
permanentFailure = runPermanentFailure
},
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(error).to(matchError(AttachmentError.notUploaded))
expect(permanentFailure).to(beTrue())
}
// MARK: ---- with a pending upload
context("with a pending upload") {
beforeEach {
mockStorage.write { db in
try attachment1.with(state: .uploading).save(db)
try attachment.with(state: .uploading).save(db)
}
}
// MARK: ------ it defers when trying to send with an attachment which is still pending upload
it("it defers when trying to send with an attachment which is still pending upload") {
var didDefer: Bool = false
mockStorage.write { db in
try attachment1.with(state: .uploading).save(db)
try attachment.with(state: .uploading).save(db)
}
MessageSendJob.run(
@ -341,12 +325,38 @@ class MessageSendJobSpec: QuickSpec {
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in didDefer = true },
dependencies: dependencies
using: dependencies
)
expect(didDefer).to(beTrue())
}
// MARK: ------ it defers when trying to send with an uploaded attachment that has an invalid downloadUrl
it("it defers when trying to send with an uploaded attachment that has an invalid downloadUrl") {
var didDefer: Bool = false
mockStorage.write { db in
try attachment
.with(
state: .uploaded,
downloadUrl: nil
)
.save(db)
}
MessageSendJob.run(
job,
queue: .main,
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in didDefer = true },
using: dependencies
)
expect(didDefer).to(beTrue())
}
// MARK: ------ inserts an attachment upload job before the message send job
it("inserts an attachment upload job before the message send job") {
mockJobRunner
.when {
@ -356,17 +366,7 @@ class MessageSendJobSpec: QuickSpec {
variant: .attachmentUpload
)
}
.thenReturn([
2: JobRunner.JobInfo(
variant: .attachmentUpload,
threadId: nil,
interactionId: 100,
detailsData: try! JSONEncoder().encode(AttachmentUploadJob.Details(
messageSendJobId: 1,
attachmentId: "200"
))
)
])
.thenReturn([:])
MessageSendJob.run(
job,
@ -374,7 +374,7 @@ class MessageSendJobSpec: QuickSpec {
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(mockJobRunner)
@ -392,12 +392,12 @@ class MessageSendJobSpec: QuickSpec {
attachmentId: "200"
)
),
before: job,
dependencies: dependencies
before: job
)
})
}
// MARK: ------ creates a dependency between the new job and the existing one
it("creates a dependency between the new job and the existing one") {
MessageSendJob.run(
job,
@ -405,7 +405,7 @@ class MessageSendJobSpec: QuickSpec {
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in },
dependencies: dependencies
using: dependencies
)
expect(mockStorage.read { db in try JobDependencies.fetchOne(db) })

View File

@ -44,11 +44,13 @@ class BatchRequestInfoSpec: QuickSpec {
)
]
)
let requestData: Data = try! JSONEncoder().encode(request)
let requestString: String? = String(data: requestData, encoding: .utf8)
expect(requestString)
.to(equal("[{\"path\":\"\\/batch\",\"method\":\"GET\",\"b64\":\"testBody\"}]"))
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["b64"] as? String).to(equal("testBody"))
}
it("successfully encodes a byte body") {
@ -70,11 +72,13 @@ class BatchRequestInfoSpec: QuickSpec {
)
]
)
let requestData: Data = try! JSONEncoder().encode(request)
let requestString: String? = String(data: requestData, encoding: .utf8)
expect(requestString)
.to(equal("[{\"path\":\"\\/batch\",\"method\":\"GET\",\"bytes\":[1,2,3]}]"))
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["bytes"] as? [Int]).to(equal([1, 2, 3]))
}
it("successfully encodes a JSON body") {
@ -96,11 +100,13 @@ class BatchRequestInfoSpec: QuickSpec {
)
]
)
let requestData: Data = try! JSONEncoder().encode(request)
let requestString: String? = String(data: requestData, encoding: .utf8)
expect(requestString)
.to(equal("[{\"path\":\"\\/batch\",\"method\":\"GET\",\"json\":{\"stringValue\":\"testValue\"}}]"))
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["json"] as? [String: String]).to(equal(["stringValue": "testValue"]))
}
it("strips authentication headers") {

View File

@ -16,9 +16,8 @@ class SOGSMessageSpec: QuickSpec {
var messageJson: String!
var messageData: Data!
var decoder: JSONDecoder!
var mockSign: MockSign!
var mockEd25519: MockEd25519!
var dependencies: SMKDependencies!
var mockCrypto: MockCrypto!
var dependencies: Dependencies!
beforeEach {
messageJson = """
@ -35,18 +34,16 @@ class SOGSMessageSpec: QuickSpec {
}
"""
messageData = messageJson.data(using: .utf8)!
mockSign = MockSign()
mockEd25519 = MockEd25519()
dependencies = SMKDependencies(
sign: mockSign,
ed25519: mockEd25519
mockCrypto = MockCrypto()
dependencies = Dependencies(
crypto: mockCrypto
)
decoder = JSONDecoder()
decoder.userInfo = [ Dependencies.userInfoKey: dependencies as Any ]
}
afterEach {
mockSign = nil
mockCrypto = nil
}
context("when decoding") {
@ -204,8 +201,10 @@ class SOGSMessageSpec: QuickSpec {
}
it("succeeds if it succeeds verification") {
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
mockCrypto
.when {
$0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray()))
}
.thenReturn(true)
expect {
@ -215,25 +214,31 @@ class SOGSMessageSpec: QuickSpec {
}
it("provides the correct values as parameters") {
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
mockCrypto
.when {
$0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray()))
}
.thenReturn(true)
_ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData)
expect(mockSign)
expect(mockCrypto)
.to(call(matchingParameters: true) {
$0.verify(
message: Data(base64Encoded: "VGVzdERhdGE=")!.bytes,
publicKey: Data(hex: TestConstants.publicKey).bytes,
signature: Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!.bytes
.signature(
message: Data(base64Encoded: "VGVzdERhdGE=")!.bytes,
publicKey: Data(hex: TestConstants.publicKey).bytes,
signature: Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!.bytes
)
)
})
}
it("throws if it fails verification") {
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
mockCrypto
.when {
$0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray()))
}
.thenReturn(false)
expect {
@ -245,7 +250,9 @@ class SOGSMessageSpec: QuickSpec {
context("that is unblinded") {
it("succeeds if it succeeds verification") {
mockEd25519.when { try $0.verifySignature(any(), publicKey: any(), data: any()) }.thenReturn(true)
mockCrypto
.when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) }
.thenReturn(true)
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
@ -254,22 +261,28 @@ class SOGSMessageSpec: QuickSpec {
}
it("provides the correct values as parameters") {
mockEd25519.when { try $0.verifySignature(any(), publicKey: any(), data: any()) }.thenReturn(true)
mockCrypto
.when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) }
.thenReturn(true)
_ = try? decoder.decode(OpenGroupAPI.Message.self, from: messageData)
expect(mockEd25519)
expect(mockCrypto)
.to(call(matchingParameters: true) {
try $0.verifySignature(
Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!,
publicKey: Data(hex: TestConstants.publicKey),
data: Data(base64Encoded: "VGVzdERhdGE=")!
$0.verify(
.signatureEd25519(
Data(base64Encoded: "VGVzdFNpZ25hdHVyZQ==")!,
publicKey: Data(hex: TestConstants.publicKey),
data: Data(base64Encoded: "VGVzdERhdGE=")!
)
)
})
}
it("throws if it fails verification") {
mockEd25519.when { try $0.verifySignature(any(), publicKey: any(), data: any()) }.thenReturn(false)
mockCrypto
.when { $0.verify(.signatureEd25519(any(), publicKey: any(), data: any())) }
.thenReturn(false)
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,98 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
class SodiumProtocolsSpec: QuickSpec {
// MARK: - Spec
override func spec() {
describe("an AeadXChaCha20Poly1305IetfType") {
let testValue: [UInt8] = [1, 2, 3]
it("provides the default values in it's extensions") {
let mockAead: MockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf()
mockAead
.when {
$0.encrypt(
message: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
additionalData: anyArray()
)
}
.thenReturn(testValue)
mockAead
.when {
$0.decrypt(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
additionalData: anyArray()
)
}
.thenReturn(testValue)
_ = mockAead.encrypt(message: [], secretKey: [], nonce: [])
_ = mockAead.decrypt(authenticatedCipherText: [], secretKey: [], nonce: [])
expect(mockAead)
.to(call {
$0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray(), additionalData: anyArray())
})
expect(mockAead)
.to(call {
$0.decrypt(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
additionalData: anyArray()
)
})
}
}
describe("a GenericHashType") {
let testValue: [UInt8] = [1, 2, 3]
it("provides the default values in it's extensions") {
let mockGenericHash: MockGenericHash = MockGenericHash()
mockGenericHash
.when { $0.hash(message: anyArray(), key: anyArray()) }
.thenReturn(testValue)
mockGenericHash
.when {
$0.hashSaltPersonal(
message: anyArray(),
outputLength: any(),
key: anyArray(),
salt: anyArray(),
personal: anyArray()
)
}
.thenReturn(testValue)
_ = mockGenericHash.hash(message: [])
_ = mockGenericHash.hashSaltPersonal(message: [], outputLength: 0, salt: [], personal: [])
expect(mockGenericHash)
.to(call { $0.hash(message: anyArray(), key: anyArray()) })
expect(mockGenericHash)
.to(call {
$0.hashSaltPersonal(
message: anyArray(),
outputLength: any(),
key: anyArray(),
salt: anyArray(),
personal: anyArray()
)
})
}
}
}
}

View File

@ -15,93 +15,110 @@ class MessageReceiverDecryptionSpec: QuickSpec {
override func spec() {
var mockStorage: Storage!
var mockSodium: MockSodium!
var mockBox: MockBox!
var mockGenericHash: MockGenericHash!
var mockSign: MockSign!
var mockAeadXChaCha: MockAeadXChaCha20Poly1305Ietf!
var mockNonce24Generator: MockNonce24Generator!
var dependencies: SMKDependencies!
var mockCrypto: MockCrypto!
var dependencies: Dependencies!
describe("a MessageReceiver") {
beforeEach {
mockStorage = Storage(
mockStorage = SynchronousStorage(
customWriter: try! DatabaseQueue(),
customMigrationTargets: [
SNUtilitiesKit.self,
SNMessagingKit.self
]
)
mockSodium = MockSodium()
mockBox = MockBox()
mockGenericHash = MockGenericHash()
mockSign = MockSign()
mockAeadXChaCha = MockAeadXChaCha20Poly1305Ietf()
mockNonce24Generator = MockNonce24Generator()
mockAeadXChaCha
.when { $0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
.thenReturn(nil)
dependencies = SMKDependencies(
mockCrypto = MockCrypto()
dependencies = Dependencies(
storage: mockStorage,
sodium: mockSodium,
box: mockBox,
genericHash: mockGenericHash,
sign: mockSign,
aeadXChaCha20Poly1305Ietf: mockAeadXChaCha,
nonceGenerator24: mockNonce24Generator
crypto: mockCrypto
)
mockStorage.write { db in
try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db)
try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db)
}
mockBox
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.encryptAeadXChaCha20(
message: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
using: dependencies
)
)
}
.thenReturn(nil)
mockCrypto
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
try $0.perform(
.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
)
}
.thenReturn([UInt8](repeating: 0, count: 100))
mockSodium
.when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) }
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(
KeyPair(
publicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
using: dependencies
)
)
}
.thenReturn([])
mockSodium
.when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) }
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(.generateBlindingFactor(serverPublicKey: any(), using: dependencies))
}
.thenReturn([])
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
mockCrypto
.when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) }
.thenReturn(Data(hex: TestConstants.blindedPublicKey).bytes)
mockSign
.when { $0.toX25519(ed25519PublicKey: anyArray()) }
mockCrypto
.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }
.thenReturn(Data(hex: TestConstants.publicKey).bytes)
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
mockCrypto
.when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) }
.thenReturn(true)
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
mockCrypto
.when {
try $0.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray()
)
)
}
.thenReturn("TestMessage".data(using: .utf8)!.bytes + [UInt8](repeating: 0, count: 32))
mockNonce24Generator
.when { $0.nonce() }
mockCrypto.when { $0.size(.nonce24) }.thenReturn(24)
mockCrypto.when { $0.size(.publicKey) }.thenReturn(32)
mockCrypto.when { $0.size(.signature) }.thenReturn(64)
mockCrypto
.when { try $0.perform(.generateNonce24()) }
.thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes)
}
@ -117,7 +134,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
),
dependencies: SMKDependencies()
using: Dependencies()
)
expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage"))
@ -126,12 +143,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot open the message") {
mockBox
mockCrypto
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
try $0.perform(
.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
)
}
.thenReturn(nil)
@ -143,19 +162,21 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
),
dependencies: dependencies
using: dependencies
)
}
.to(throwError(MessageReceiverError.decryptionFailed))
}
it("throws an error if the open message is too short") {
mockBox
mockCrypto
.when {
$0.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
try $0.perform(
.open(
anonymousCipherText: anyArray(),
recipientPublicKey: anyArray(),
recipientSecretKey: anyArray()
)
)
}
.thenReturn([1, 2, 3])
@ -167,15 +188,15 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
),
dependencies: dependencies
using: dependencies
)
}
.to(throwError(MessageReceiverError.decryptionFailed))
}
it("throws an error if it cannot verify the message") {
mockSign
.when { $0.verify(message: anyArray(), publicKey: anyArray(), signature: anyArray()) }
mockCrypto
.when { $0.verify(.signature(message: anyArray(), publicKey: anyArray(), signature: anyArray())) }
.thenReturn(false)
expect {
@ -185,14 +206,14 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
),
dependencies: dependencies
using: dependencies
)
}
.to(throwError(MessageReceiverError.invalidSignature))
}
it("throws an error if it cannot get the senders x25519 public key") {
mockSign.when { $0.toX25519(ed25519PublicKey: anyArray()) }.thenReturn(nil)
mockCrypto.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }.thenReturn(nil)
expect {
try MessageReceiver.decryptWithSessionProtocol(
@ -201,7 +222,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.privateKey)!.bytes
),
dependencies: dependencies
using: dependencies
)
}
.to(throwError(MessageReceiverError.decryptionFailed))
@ -223,7 +244,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
publicKey: Data.data(fromHex: TestConstants.edPublicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
),
using: SMKDependencies()
using: Dependencies()
)
expect(String(data: (result?.plaintext ?? Data()), encoding: .utf8)).to(equal("TestMessage"))
@ -271,8 +292,16 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot get the blinded keyPair") {
mockSodium
.when { $0.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), genericHash: mockGenericHash) }
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(nil)
expect {
@ -296,14 +325,16 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot get the decryption key") {
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
using: dependencies
)
)
}
.thenReturn(nil)
@ -350,8 +381,16 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot decrypt the data") {
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
mockCrypto
.when {
try $0.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray()
)
)
}
.thenReturn(nil)
expect {
@ -375,8 +414,16 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if the inner bytes are too short") {
mockAeadXChaCha
.when { $0.decrypt(authenticatedCipherText: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
mockCrypto
.when {
try $0.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: anyArray(),
secretKey: anyArray(),
nonce: anyArray()
)
)
}
.thenReturn([1, 2, 3])
expect {
@ -400,8 +447,10 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot generate the blinding factor") {
mockSodium
.when { $0.generateBlindingFactor(serverPublicKey: any(), genericHash: mockGenericHash) }
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(.generateBlindingFactor(serverPublicKey: any(), using: dependencies))
}
.thenReturn(nil)
expect {
@ -425,8 +474,8 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot generate the combined key") {
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
mockCrypto
.when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) }
.thenReturn(nil)
expect {
@ -450,8 +499,8 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if the combined key does not match kA") {
mockSodium
.when { $0.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray()) }
mockCrypto
.when { try $0.perform(.combineKeys(lhsKeyBytes: anyArray(), rhsKeyBytes: anyArray())) }
.thenReturn(Data(hex: TestConstants.publicKey).bytes)
expect {
@ -475,8 +524,8 @@ class MessageReceiverDecryptionSpec: QuickSpec {
}
it("throws an error if it cannot get the senders x25519 public key") {
mockSign
.when { $0.toX25519(ed25519PublicKey: anyArray()) }
mockCrypto
.when { try $0.perform(.toX25519(ed25519PublicKey: anyArray())) }
.thenReturn(nil)
expect {

View File

@ -15,53 +15,55 @@ class MessageSenderEncryptionSpec: QuickSpec {
override func spec() {
var mockStorage: Storage!
var mockBox: MockBox!
var mockSign: MockSign!
var mockNonce24Generator: MockNonce24Generator!
var dependencies: SMKDependencies!
var mockCrypto: MockCrypto!
var dependencies: Dependencies!
describe("a MessageSender") {
// MARK: - Configuration
beforeEach {
mockStorage = Storage(
mockStorage = SynchronousStorage(
customWriter: try! DatabaseQueue(),
customMigrationTargets: [
SNUtilitiesKit.self,
SNMessagingKit.self
]
)
mockBox = MockBox()
mockSign = MockSign()
mockNonce24Generator = MockNonce24Generator()
mockCrypto = MockCrypto()
dependencies = SMKDependencies(
dependencies = Dependencies(
storage: mockStorage,
box: mockBox,
sign: mockSign,
nonceGenerator24: mockNonce24Generator
crypto: mockCrypto
)
mockStorage.write { db in
try Identity(variant: .ed25519PublicKey, data: Data(hex: TestConstants.edPublicKey)).insert(db)
try Identity(variant: .ed25519SecretKey, data: Data(hex: TestConstants.edSecretKey)).insert(db)
}
mockNonce24Generator
.when { $0.nonce() }
mockCrypto
.when { try $0.perform(.generateNonce24()) }
.thenReturn(Data(base64Encoded: "pbTUizreT0sqJ2R2LloseQDyVL2RYztD")!.bytes)
}
// MARK: - when encrypting with the session protocol
context("when encrypting with the session protocol") {
beforeEach {
mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn([1, 2, 3])
mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn([])
mockCrypto
.when { try $0.perform(.seal(message: anyArray(), recipientPublicKey: anyArray())) }
.thenReturn([1, 2, 3])
mockCrypto
.when { try $0.perform(.signature(message: anyArray(), secretKey: anyArray())) }
.thenReturn([])
}
// MARK: -- can encrypt correctly
it("can encrypt correctly") {
let result = mockStorage.write { db in
let result: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
for: "05\(TestConstants.publicKey)",
using: SMKDependencies(storage: mockStorage)
using: Dependencies() // Don't mock
)
}
@ -70,8 +72,9 @@ class MessageSenderEncryptionSpec: QuickSpec {
expect(result?.count).to(equal(155))
}
// MARK: -- returns the correct value when mocked
it("returns the correct value when mocked") {
let result = mockStorage.write { db in
let result: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
@ -83,13 +86,14 @@ class MessageSenderEncryptionSpec: QuickSpec {
expect(result?.bytes).to(equal([1, 2, 3]))
}
// MARK: -- throws an error if there is no ed25519 keyPair
it("throws an error if there is no ed25519 keyPair") {
mockStorage.write { db in
_ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db)
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionProtocol(
db,
@ -102,10 +106,13 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if the signature generation fails
it("throws an error if the signature generation fails") {
mockSign.when { $0.signature(message: anyArray(), secretKey: anyArray()) }.thenReturn(nil)
mockCrypto
.when { try $0.perform(.signature(message: anyArray(), secretKey: anyArray())) }
.thenReturn(nil)
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionProtocol(
db,
@ -118,10 +125,13 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if the encryption fails
it("throws an error if the encryption fails") {
mockBox.when { $0.seal(message: anyArray(), recipientPublicKey: anyArray()) }.thenReturn(nil)
mockCrypto
.when { try $0.perform(.seal(message: anyArray(), recipientPublicKey: anyArray())) }
.thenReturn(nil)
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionProtocol(
db,
@ -135,9 +145,67 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: - when encrypting with the blinded session protocol
context("when encrypting with the blinded session protocol") {
it("successfully encrypts") {
let result = mockStorage.write { db in
beforeEach {
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(.blindedKeyPair(serverPublicKey: any(), edKeyPair: any(), using: dependencies))
}
.thenReturn(
KeyPair(
publicKey: Data.data(fromHex: TestConstants.publicKey)!.bytes,
secretKey: Data.data(fromHex: TestConstants.edSecretKey)!.bytes
)
)
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
using: dependencies
)
)
}
.thenReturn([1, 2, 3])
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.encryptAeadXChaCha20(
message: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
additionalData: anyArray(),
using: dependencies
)
)
}
.thenReturn([2, 3, 4])
}
// MARK: -- can encrypt correctly
it("can encrypt correctly") {
let result: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionBlindingProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
for: "15\(TestConstants.blindedPublicKey)",
openGroupPublicKey: TestConstants.serverPublicKey,
using: Dependencies() // Don't mock
)
}
// Note: A Nonce is used for this so we can't compare the exact value when not mocked
expect(result).toNot(beNil())
expect(result?.count).to(equal(84))
}
// MARK: -- returns the correct value when mocked
it("returns the correct value when mocked") {
let result: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionBlindingProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
@ -148,15 +216,12 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
expect(result?.toHexString())
.to(equal(
"00db16b6687382811d69875a5376f66acad9c49fe5e26bcf770c7e6e9c230299" +
"f61b315299dd1fa700dd7f34305c0465af9e64dc791d7f4123f1eeafa5b4d48b" +
"3ade4f4b2a2764762e5a2c7900f254bd91633b43"
))
.to(equal("00020304a5b4d48b3ade4f4b2a2764762e5a2c7900f254bd91633b43"))
}
// MARK: -- includes a version at the start of the encrypted value
it("includes a version at the start of the encrypted value") {
let result = mockStorage.write { db in
let result: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionBlindingProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
@ -169,8 +234,9 @@ class MessageSenderEncryptionSpec: QuickSpec {
expect(result?.toHexString().prefix(2)).to(equal("00"))
}
// MARK: -- includes the nonce at the end of the encrypted value
it("includes the nonce at the end of the encrypted value") {
let maybeResult = mockStorage.write { db in
let maybeResult: Data? = mockStorage.read { db in
try? MessageSender.encryptWithSessionBlindingProtocol(
db,
plaintext: "TestMessage".data(using: .utf8)!,
@ -186,8 +252,9 @@ class MessageSenderEncryptionSpec: QuickSpec {
.to(equal("pbTUizreT0sqJ2R2LloseQDyVL2RYztD"))
}
// MARK: -- throws an error if the recipient isn't a blinded id
it("throws an error if the recipient isn't a blinded id") {
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
db,
@ -201,13 +268,14 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if there is no ed25519 keyPair
it("throws an error if there is no ed25519 keyPair") {
mockStorage.write { db in
_ = try Identity.filter(id: .ed25519PublicKey).deleteAll(db)
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
db,
@ -221,22 +289,21 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if it fails to generate a blinded keyPair
it("throws an error if it fails to generate a blinded keyPair") {
let mockSodium: MockSodium = MockSodium()
let mockGenericHash: MockGenericHash = MockGenericHash()
dependencies = dependencies.with(sodium: mockSodium, genericHash: mockGenericHash)
mockSodium
.when {
$0.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
genericHash: mockGenericHash
mockCrypto
.when { [dependencies = dependencies!] crypto in
crypto.generate(
.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
using: dependencies
)
)
}
.thenReturn(nil)
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
db,
@ -250,38 +317,23 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if it fails to generate an encryption key
it("throws an error if it fails to generate an encryption key") {
let mockSodium: MockSodium = MockSodium()
let mockGenericHash: MockGenericHash = MockGenericHash()
dependencies = dependencies.with(sodium: mockSodium, genericHash: mockGenericHash)
mockSodium
.when {
$0.blindedKeyPair(
serverPublicKey: any(),
edKeyPair: any(),
genericHash: mockGenericHash
)
}
.thenReturn(
KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
)
)
mockSodium
.when {
$0.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
genericHash: mockGenericHash
mockCrypto
.when { [dependencies = dependencies!] crypto in
try crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: anyArray(),
otherBlindedPublicKey: anyArray(),
fromBlindedPublicKey: anyArray(),
toBlindedPublicKey: anyArray(),
using: dependencies
)
)
}
.thenReturn(nil)
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
db,
@ -295,15 +347,23 @@ class MessageSenderEncryptionSpec: QuickSpec {
}
}
// MARK: -- throws an error if it fails to encrypt
it("throws an error if it fails to encrypt") {
let mockAeadXChaCha: MockAeadXChaCha20Poly1305Ietf = MockAeadXChaCha20Poly1305Ietf()
dependencies = dependencies.with(aeadXChaCha20Poly1305Ietf: mockAeadXChaCha)
mockAeadXChaCha
.when { $0.encrypt(message: anyArray(), secretKey: anyArray(), nonce: anyArray()) }
mockCrypto
.when {
try $0.perform(
.encryptAeadXChaCha20(
message: anyArray(),
secretKey: anyArray(),
nonce: anyArray(),
additionalData: anyArray(),
using: dependencies
)
)
}
.thenReturn(nil)
mockStorage.write { db in
mockStorage.read { db in
expect {
try MessageSender.encryptWithSessionBlindingProtocol(
db,

View File

@ -0,0 +1,402 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
class CryptoSMKSpec: QuickSpec {
// MARK: - Spec
override func spec() {
var crypto: Crypto!
var mockCrypto: MockCrypto!
var dependencies: Dependencies!
beforeEach {
crypto = Crypto()
mockCrypto = MockCrypto()
dependencies = Dependencies(crypto: crypto)
}
describe("Crypto for SessionMessagingKit") {
// MARK: - when extending Sign
context("when extending Sign") {
// MARK: -- can convert an ed25519 public key into an x25519 public key
it("can convert an ed25519 public key into an x25519 public key") {
let result = try? crypto.perform(.toX25519(ed25519PublicKey: TestConstants.edPublicKey.bytes))
expect(result?.toHexString())
.to(equal("95ffb559d4e804e9b414a5178454c426f616b4a61089b217b41165dbb7c9fe2d"))
}
// MARK: -- can convert an ed25519 private key into an x25519 private key
it("can convert an ed25519 private key into an x25519 private key") {
let result = try? crypto.perform(.toX25519(ed25519SecretKey: TestConstants.edSecretKey.bytes))
expect(result?.toHexString())
.to(equal("c83f9a1479b103c275d2db2d6c199fdc6f589b29b742f6405e01cc5a9a1d135d"))
}
}
// MARK: - when extending Sodium
context("when extending Sodium") {
// MARK: -- and generating a blinding factor
context("and generating a blinding factor") {
// MARK: --- successfully generates a blinding factor
it("successfully generates a blinding factor") {
let result = try? crypto.perform(
.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
)
expect(result?.toHexString())
.to(equal("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
// MARK: --- fails if the serverPublicKey is not a hex string
it("fails if the serverPublicKey is not a hex string") {
let result = try? crypto.perform(
.generateBlindingFactor(
serverPublicKey: "Test",
using: dependencies
)
)
expect(result).to(beNil())
}
// MARK: --- fails if it cannot hash the serverPublicKey bytes
it("fails if it cannot hash the serverPublicKey bytes") {
dependencies = Dependencies(crypto: mockCrypto)
mockCrypto
.when { try $0.perform(.hash(message: anyArray(), outputLength: any())) }
.thenReturn(nil)
let result = try? crypto.perform(
.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
)
expect(result).to(beNil())
}
}
// MARK: -- and generating a blinded key pair
context("and generating a blinded key pair") {
// MARK: --- successfully generates a blinded key pair
it("successfully generates a blinded key pair") {
let result = crypto.generate(
.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
using: dependencies
)
)
// Note: The first 64 characters of the secretKey are consistent but the chars after that always differ
expect(result?.publicKey.toHexString()).to(equal(TestConstants.blindedPublicKey))
expect(String(result?.secretKey.toHexString().prefix(64) ?? ""))
.to(equal("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
// MARK: --- fails if the edKeyPair public key length wrong
it("fails if the edKeyPair public key length wrong") {
let result = crypto.generate(
.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: String(TestConstants.edPublicKey.prefix(4))).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
using: dependencies
)
)
expect(result).to(beNil())
}
// MARK: --- fails if the edKeyPair secret key length wrong
it("fails if the edKeyPair secret key length wrong") {
let result = crypto.generate(
.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes
),
using: dependencies
)
)
expect(result).to(beNil())
}
// MARK: --- fails if it cannot generate a blinding factor
it("fails if it cannot generate a blinding factor") {
let result = crypto.generate(
.blindedKeyPair(
serverPublicKey: "Test",
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
using: dependencies
)
)
expect(result).to(beNil())
}
}
// MARK: -- and generating a sogsSignature
context("and generating a sogsSignature") {
// MARK: --- generates a correct signature
it("generates a correct signature") {
let result = try? crypto.perform(
.sogsSignature(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
blindedSecretKey: Data(hex: "44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409").bytes,
blindedPublicKey: Data(hex: "0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2").bytes
)
)
expect(result?.toHexString())
.to(equal(
"dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9" +
"072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
))
}
}
// MARK: -- and combining keys
context("and combining keys") {
// MARK: --- generates a correct combined key
it("generates a correct combined key") {
let result = try? crypto.perform(
.combineKeys(
lhsKeyBytes: Data(hex: TestConstants.edSecretKey).bytes,
rhsKeyBytes: Data(hex: TestConstants.edPublicKey).bytes
)
)
expect(result?.toHexString())
.to(equal("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
}
// MARK: -- and creating a shared blinded encryption key
context("and creating a shared blinded encryption key") {
// MARK: --- generates a correct combined key
it("generates a correct combined key") {
let result = try? crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
using: dependencies
)
)
expect(result?.toHexString())
.to(equal("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
// MARK: --- fails if the scalar multiplication fails
it("fails if the scalar multiplication fails") {
let result = try? crypto.perform(
.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.edPublicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
using: dependencies
)
)
expect(result?.toHexString()).to(beNil())
}
}
// MARK: -- and checking if a session id matches a blinded id
context("and checking if a session id matches a blinded id") {
// MARK: --- returns true when they match
it("returns true when they match") {
let result = crypto.verify(
.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
)
expect(result).to(beTrue())
}
// MARK: --- returns false if given an invalid session id
it("returns false if given an invalid session id") {
let result = crypto.verify(
.sessionId(
"AB\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
)
expect(result).to(beFalse())
}
// MARK: --- returns false if given an invalid blinded id
it("returns false if given an invalid blinded id") {
let result = crypto.verify(
.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "AB\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
using: dependencies
)
)
expect(result).to(beFalse())
}
// MARK: --- returns false if it fails to generate the blinding factor
it("returns false if it fails to generate the blinding factor") {
let result = crypto.verify(
.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: "Test",
using: dependencies
)
)
expect(result).to(beFalse())
}
}
}
// MARK: - when extending GenericHash
describe("when extending GenericHash") {
// MARK: -- and generating a hash with salt and personal values
context("and generating a hash with salt and personal values") {
// MARK: --- generates a hash correctly
it("generates a hash correctly") {
let result = try? crypto.perform(
.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
// MARK: --- generates a hash correctly with no key
it("generates a hash correctly with no key") {
let result = try? crypto.perform(
.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: nil,
salt: "Salt".bytes,
personal: "Personal".bytes
)
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
// MARK: --- fails if given invalid options
it("fails if given invalid options") {
let result = try? crypto.perform(
.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 65, // Max of 64
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
)
expect(result).to(beNil())
}
}
}
// MARK: - when extending AeadXChaCha20Poly1305Ietf
context("when extending AeadXChaCha20Poly1305Ietf") {
// MARK: -- when encrypting
context("when encrypting") {
// MARK: --- encrypts correctly
it("encrypts correctly") {
let result = try? crypto.perform(
.encryptAeadXChaCha20(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: nil,
using: Dependencies()
)
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
// MARK: --- encrypts correctly with additional data
it("encrypts correctly with additional data") {
let result = try? crypto.perform(
.encryptAeadXChaCha20(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes,
using: Dependencies()
)
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
// MARK: --- fails if given an invalid key
it("fails if given an invalid key") {
let result = try? crypto.perform(
.encryptAeadXChaCha20(
message: "TestMessage".bytes,
secretKey: "TestKey".bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes,
using: Dependencies()
)
)
expect(result).to(beNil())
}
}
}
}
}
}

View File

@ -1,352 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
class SodiumUtilitiesSpec: QuickSpec {
// MARK: - Spec
override func spec() {
// MARK: - Sign
describe("an extended Sign") {
var sign: Sign!
beforeEach {
sign = Sodium().sign
}
it("can convert an ed25519 public key into an x25519 public key") {
let result = sign.toX25519(ed25519PublicKey: TestConstants.edPublicKey.bytes)
expect(result?.toHexString())
.to(equal("95ffb559d4e804e9b414a5178454c426f616b4a61089b217b41165dbb7c9fe2d"))
}
it("can convert an ed25519 private key into an x25519 private key") {
let result = sign.toX25519(ed25519SecretKey: TestConstants.edSecretKey.bytes)
expect(result?.toHexString())
.to(equal("c83f9a1479b103c275d2db2d6c199fdc6f589b29b742f6405e01cc5a9a1d135d"))
}
}
// MARK: - Sodium
describe("an extended Sodium") {
var sodium: Sodium!
var genericHash: GenericHashType!
beforeEach {
sodium = Sodium()
genericHash = sodium.genericHash
}
context("when generating a blinding factor") {
it("successfully generates a blinding factor") {
let result = sodium.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result?.toHexString())
.to(equal("84e3eb75028a9b73fec031b7448e322a68ca6485fad81ab1bead56f759ebeb0f"))
}
it("fails if the serverPublicKey is not a hex string") {
let result = sodium.generateBlindingFactor(
serverPublicKey: "Test",
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if it cannot hash the serverPublicKey bytes") {
genericHash = MockGenericHash()
(genericHash as? MockGenericHash)?
.when { $0.hash(message: anyArray(), outputLength: any()) }
.thenReturn(nil)
let result = sodium.generateBlindingFactor(
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beNil())
}
}
context("when generating a blinded key pair") {
it("successfully generates a blinded key pair") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
// Note: The first 64 characters of the secretKey are consistent but the chars after that always differ
expect(result?.publicKey.toHexString()).to(equal(TestConstants.blindedPublicKey))
expect(String(result?.secretKey.toHexString().prefix(64) ?? ""))
.to(equal("16663322d6b684e1c9dcc02b9e8642c3affd3bc431a9ea9e63dbbac88ce7a305"))
}
it("fails if the edKeyPair public key length wrong") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: String(TestConstants.edPublicKey.prefix(4))).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if the edKeyPair secret key length wrong") {
let result = sodium.blindedKeyPair(
serverPublicKey: TestConstants.serverPublicKey,
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: String(TestConstants.edSecretKey.prefix(4))).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
it("fails if it cannot generate a blinding factor") {
let result = sodium.blindedKeyPair(
serverPublicKey: "Test",
edKeyPair: KeyPair(
publicKey: Data(hex: TestConstants.edPublicKey).bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes
),
genericHash: genericHash
)
expect(result).to(beNil())
}
}
context("when generating a sogsSignature") {
it("generates a correct signature") {
let result = sodium.sogsSignature(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
blindedSecretKey: Data(hex: "44d82cc15c0a5056825cae7520b6b52d000a23eb0c5ed94c4be2d9dc41d2d409").bytes,
blindedPublicKey: Data(hex: "0bb7815abb6ba5142865895f3e5286c0527ba4d31dbb75c53ce95e91ffe025a2").bytes
)
expect(result?.toHexString())
.to(equal(
"dcc086abdd2a740d9260b008fb37e12aa0ff47bd2bd9e177bbbec37fd46705a9" +
"072ce747bda66c788c3775cdd7ad60ad15a478e0886779aad5d795fd7bf8350d"
))
}
}
context("when combining keys") {
it("generates a correct combined key") {
let result = sodium.combineKeys(
lhsKeyBytes: Data(hex: TestConstants.edSecretKey).bytes,
rhsKeyBytes: Data(hex: TestConstants.edPublicKey).bytes
)
expect(result?.toHexString())
.to(equal("1159b5d0fcfba21228eb2121a0f59712fa8276fc6e5547ff519685a40b9819e6"))
}
it("fails if the scalar multiplication fails") {
let result = sodium.combineKeys(
lhsKeyBytes: sodium.generatePrivateKeyScalar(secretKey: Data(hex: TestConstants.edSecretKey).bytes),
rhsKeyBytes: Data(hex: TestConstants.publicKey).bytes
)
expect(result).to(beNil())
}
}
context("when creating a shared blinded encryption key") {
it("generates a correct combined key") {
let result = sodium.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.blindedPublicKey).bytes,
genericHash: genericHash
)
expect(result?.toHexString())
.to(equal("388ee09e4c356b91f1cce5cc0aa0cf59e8e8cade69af61685d09c2d2731bc99e"))
}
it("fails if the scalar multiplication fails") {
let result = sodium.sharedBlindedEncryptionKey(
secretKey: Data(hex: TestConstants.edSecretKey).bytes,
otherBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
fromBlindedPublicKey: Data(hex: TestConstants.edPublicKey).bytes,
toBlindedPublicKey: Data(hex: TestConstants.publicKey).bytes,
genericHash: genericHash
)
expect(result?.toHexString()).to(beNil())
}
}
context("when checking if a session id matches a blinded id") {
it("returns true when they match") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beTrue())
}
it("returns false if given an invalid session id") {
let result = sodium.sessionId(
"AB\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beFalse())
}
it("returns false if given an invalid blinded id") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "AB\(TestConstants.blindedPublicKey)",
serverPublicKey: TestConstants.serverPublicKey,
genericHash: genericHash
)
expect(result).to(beFalse())
}
it("returns false if it fails to generate the blinding factor") {
let result = sodium.sessionId(
"05\(TestConstants.publicKey)",
matchesBlindedId: "15\(TestConstants.blindedPublicKey)",
serverPublicKey: "Test",
genericHash: genericHash
)
expect(result).to(beFalse())
}
}
}
// MARK: - GenericHash
describe("an extended GenericHash") {
var genericHash: GenericHashType!
beforeEach {
genericHash = Sodium().genericHash
}
context("when generating a hash with salt and personal values") {
it("generates a hash correctly") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
it("generates a hash correctly with no key") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 32,
key: nil,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(32))
}
it("fails if given invalid options") {
let result = genericHash.hashSaltPersonal(
message: "TestMessage".bytes,
outputLength: 65, // Max of 64
key: "Key".bytes,
salt: "Salt".bytes,
personal: "Personal".bytes
)
expect(result).to(beNil())
}
}
}
// MARK: - AeadXChaCha20Poly1305IetfType
describe("an extended AeadXChaCha20Poly1305IetfType") {
var aeadXchacha20poly1305ietf: AeadXChaCha20Poly1305IetfType!
beforeEach {
aeadXchacha20poly1305ietf = Sodium().aead.xchacha20poly1305ietf
}
context("when encrypting") {
it("encrypts correctly") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: nil
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
it("encrypts correctly with additional data") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: Data(hex: TestConstants.publicKey).bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes
)
expect(result).toNot(beNil())
expect(result?.count).to(equal(27))
}
it("fails if given an invalid key") {
let result = aeadXchacha20poly1305ietf.encrypt(
message: "TestMessage".bytes,
secretKey: "TestKey".bytes,
nonce: "TestNonce".bytes,
additionalData: "TestData".bytes
)
expect(result).to(beNil())
}
}
}
}
}

View File

@ -1,44 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
@testable import SessionMessagingKit
extension SMKDependencies {
public func with(
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: MutableGeneralCacheType? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) -> SMKDependencies {
return SMKDependencies(
onionApi: (onionApi ?? self._onionApi.wrappedValue),
generalCache: (generalCache ?? self._mutableGeneralCache.wrappedValue),
storage: (storage ?? self._storage.wrappedValue),
scheduler: (scheduler ?? self._scheduler.wrappedValue),
sodium: (sodium ?? self._sodium.wrappedValue),
box: (box ?? self._box.wrappedValue),
genericHash: (genericHash ?? self._genericHash.wrappedValue),
sign: (sign ?? self._sign.wrappedValue),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue),
ed25519: (ed25519 ?? self._ed25519.wrappedValue),
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue),
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue),
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue),
date: (date ?? self._date.wrappedValue)
)
}
}

View File

@ -1,19 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
@testable import SessionMessagingKit
class MockAeadXChaCha20Poly1305Ietf: Mock<AeadXChaCha20Poly1305IetfType>, AeadXChaCha20Poly1305IetfType {
var KeyBytes: Int = 32
var ABytes: Int = 16
func encrypt(message: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? {
return accept(args: [message, secretKey, nonce, additionalData]) as? Bytes
}
func decrypt(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes, additionalData: Bytes?) -> Bytes? {
return accept(args: [authenticatedCipherText, secretKey, nonce, additionalData]) as? Bytes
}
}

View File

@ -1,16 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
@testable import SessionMessagingKit
class MockBox: Mock<BoxType>, BoxType {
func seal(message: Bytes, recipientPublicKey: Bytes) -> Bytes? {
return accept(args: [message, recipientPublicKey]) as? Bytes
}
func open(anonymousCipherText: Bytes, recipientPublicKey: Bytes, recipientSecretKey: Bytes) -> Bytes? {
return accept(args: [anonymousCipherText, recipientPublicKey, recipientSecretKey]) as? Bytes
}
}

View File

@ -1,17 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
@testable import SessionMessagingKit
class MockEd25519: Mock<Ed25519Type>, Ed25519Type {
func sign(data: Bytes, keyPair: KeyPair) throws -> Bytes? {
return accept(args: [data, keyPair]) as? Bytes
}
func verifySignature(_ signature: Data, publicKey: Data, data: Data) throws -> Bool {
return accept(args: [signature, publicKey, data]) as! Bool
}
}

View File

@ -1,20 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
@testable import SessionMessagingKit
class MockGenericHash: Mock<GenericHashType>, GenericHashType {
func hash(message: Bytes, key: Bytes?) -> Bytes? {
return accept(args: [message, key]) as? Bytes
}
func hash(message: Bytes, outputLength: Int) -> Bytes? {
return accept(args: [message, outputLength]) as? Bytes
}
func hashSaltPersonal(message: Bytes, outputLength: Int, key: Bytes?, salt: Bytes, personal: Bytes) -> Bytes? {
return accept(args: [message, outputLength, key, salt, personal]) as? Bytes
}
}

View File

@ -1,11 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@testable import SessionMessagingKit
class MockNonce16Generator: Mock<NonceGenerator16ByteType>, NonceGenerator16ByteType {
var NonceBytes: Int = 16
func nonce() -> Array<UInt8> { return accept() as! [UInt8] }
}

View File

@ -1,11 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
@testable import SessionMessagingKit
class MockNonce24Generator: Mock<NonceGenerator24ByteType>, NonceGenerator24ByteType {
var NonceBytes: Int = 24
func nonce() -> Array<UInt8> { return accept() as! [UInt8] }
}

View File

@ -6,7 +6,7 @@ import SessionUtilitiesKit
@testable import SessionMessagingKit
class MockOGMCache: Mock<OGMMutableCacheType>, OGMMutableCacheType {
class MockOGMCache: Mock<OGMCacheType>, OGMCacheType {
var defaultRoomsPublisher: AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error>? {
get { return accept() as? AnyPublisher<[OpenGroupManager.DefaultRoomInfo], Error> }
set { accept(args: [newValue]) }

View File

@ -1,23 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
@testable import SessionMessagingKit
class MockSign: Mock<SignType>, SignType {
var Bytes: Int = 64
var PublicKeyBytes: Int = 32
func signature(message: Bytes, secretKey: Bytes) -> Bytes? {
return accept(args: [message, secretKey]) as? Bytes
}
func verify(message: Bytes, publicKey: Bytes, signature: Bytes) -> Bool {
return accept(args: [message, publicKey, signature]) as! Bool
}
func toX25519(ed25519PublicKey: Bytes) -> Bytes? {
return accept(args: [ed25519PublicKey]) as? Bytes
}
}

View File

@ -1,38 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtilitiesKit
@testable import SessionMessagingKit
class MockSodium: Mock<SodiumType>, SodiumType {
func getBox() -> BoxType { return accept() as! BoxType }
func getGenericHash() -> GenericHashType { return accept() as! GenericHashType }
func getSign() -> SignType { return accept() as! SignType }
func getAeadXChaCha20Poly1305Ietf() -> AeadXChaCha20Poly1305IetfType { return accept() as! AeadXChaCha20Poly1305IetfType }
func generateBlindingFactor(serverPublicKey: String, genericHash: GenericHashType) -> Bytes? {
return accept(args: [serverPublicKey, genericHash]) as? Bytes
}
func blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair, genericHash: GenericHashType) -> KeyPair? {
return accept(args: [serverPublicKey, edKeyPair, genericHash]) as? KeyPair
}
func sogsSignature(message: Bytes, secretKey: Bytes, blindedSecretKey ka: Bytes, blindedPublicKey kA: Bytes) -> Bytes? {
return accept(args: [message, secretKey, ka, kA]) as? Bytes
}
func combineKeys(lhsKeyBytes: Bytes, rhsKeyBytes: Bytes) -> Bytes? {
return accept(args: [lhsKeyBytes, rhsKeyBytes]) as? Bytes
}
func sharedBlindedEncryptionKey(secretKey a: Bytes, otherBlindedPublicKey: Bytes, fromBlindedPublicKey kA: Bytes, toBlindedPublicKey kB: Bytes, genericHash: GenericHashType) -> Bytes? {
return accept(args: [a, otherBlindedPublicKey, kA, kB, genericHash]) as? Bytes
}
func sessionId(_ sessionId: String, matchesBlindedId blindedSessionId: String, serverPublicKey: String, genericHash: GenericHashType) -> Bool {
return accept(args: [sessionId, blindedSessionId, serverPublicKey, genericHash]) as! Bool
}
}

View File

@ -1,46 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionSnodeKit
import SessionUtilitiesKit
@testable import SessionMessagingKit
extension OpenGroupManager.OGMDependencies {
public func with(
cache: OGMMutableCacheType? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: MutableGeneralCacheType? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
sodium: SodiumType? = nil,
box: BoxType? = nil,
genericHash: GenericHashType? = nil,
sign: SignType? = nil,
aeadXChaCha20Poly1305Ietf: AeadXChaCha20Poly1305IetfType? = nil,
ed25519: Ed25519Type? = nil,
nonceGenerator16: NonceGenerator16ByteType? = nil,
nonceGenerator24: NonceGenerator24ByteType? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) -> OpenGroupManager.OGMDependencies {
return OpenGroupManager.OGMDependencies(
cache: (cache ?? self._mutableCache.wrappedValue),
onionApi: (onionApi ?? self._onionApi.wrappedValue),
generalCache: (generalCache ?? self._mutableGeneralCache.wrappedValue),
storage: (storage ?? self._storage.wrappedValue),
scheduler: (scheduler ?? self._scheduler.wrappedValue),
sodium: (sodium ?? self._sodium.wrappedValue),
box: (box ?? self._box.wrappedValue),
genericHash: (genericHash ?? self._genericHash.wrappedValue),
sign: (sign ?? self._sign.wrappedValue),
aeadXChaCha20Poly1305Ietf: (aeadXChaCha20Poly1305Ietf ?? self._aeadXChaCha20Poly1305Ietf.wrappedValue),
ed25519: (ed25519 ?? self._ed25519.wrappedValue),
nonceGenerator16: (nonceGenerator16 ?? self._nonceGenerator16.wrappedValue),
nonceGenerator24: (nonceGenerator24 ?? self._nonceGenerator24.wrappedValue),
standardUserDefaults: (standardUserDefaults ?? self._standardUserDefaults.wrappedValue),
date: (date ?? self._date.wrappedValue)
)
}
}

View File

@ -1,82 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionSnodeKit
import SessionUtilitiesKit
@testable import SessionMessagingKit
// FIXME: Change 'OnionRequestAPIType' to have instance methods instead of static methods once everything is updated to use 'Dependencies'
class TestOnionRequestAPI: OnionRequestAPIType {
struct RequestData: Codable {
let urlString: String?
let httpMethod: String
let headers: [String: String]
let body: Data?
let destination: OnionRequestAPIDestination
var publicKey: String? {
switch destination {
case .snode: return nil
case .server(_, _, let x25519PublicKey, _, _): return x25519PublicKey
}
}
}
class ResponseInfo: ResponseInfoType {
let requestData: RequestData
let code: Int
let headers: [String: String]
init(requestData: RequestData, code: Int, headers: [String: String]) {
self.requestData = requestData
self.code = code
self.headers = headers
}
}
class var mockResponse: Data? { return nil }
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: request.url?.absoluteString,
httpMethod: (request.httpMethod ?? "GET"),
headers: (request.allHTTPHeaderFields ?? [:]),
body: request.httpBody,
destination: OnionRequestAPIDestination.server(
host: (request.url?.host ?? ""),
target: OnionRequestAPIVersion.v4.rawValue,
x25519PublicKey: x25519PublicKey,
scheme: request.url!.scheme,
port: request.url!.port.map { UInt16($0) }
)
),
code: 200,
headers: [:]
)
return Just((responseInfo, mockResponse))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
static func sendOnionRequest(_ payload: Data, to snode: Snode, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: "\(snode.address):\(snode.port)/onion_req/v2",
httpMethod: "POST",
headers: [:],
body: payload,
destination: OnionRequestAPIDestination.snode(snode)
),
code: 200,
headers: [:]
)
return Just((responseInfo, mockResponse))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}

View File

@ -175,7 +175,13 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
func attachmentApproval(
_ attachmentApproval: AttachmentApprovalViewController,
didApproveAttachments attachments: [SignalAttachment],
forThreadId threadId: String,
messageText: String?,
using dependencies: Dependencies = Dependencies()
) {
// Sharing a URL or plain text will populate the 'messageText' field so in those
// cases we should ignore the attachments
let isSharingUrl: Bool = (attachments.count == 1 && attachments[0].isUrl)
@ -198,7 +204,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
// Resume database
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
Storage.shared
dependencies.storage
.writePublisher { db -> MessageSender.PreparedSendData in
guard
let threadVariant: SessionThread.Variant = try SessionThread
@ -262,12 +268,13 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
db,
interaction: interaction,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0, using: dependencies) }
.flatMap { MessageSender.sendImmediate(data: $0, using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(

View File

@ -76,11 +76,16 @@ public extension SnodeReceivedMessageInfo {
// MARK: - GRDB Interactions
public extension SnodeReceivedMessageInfo {
static func pruneExpiredMessageHashInfo(for snode: Snode, namespace: SnodeAPI.Namespace, associatedWith publicKey: String) {
static func pruneExpiredMessageHashInfo(
for snode: Snode,
namespace: SnodeAPI.Namespace,
associatedWith publicKey: String,
using dependencies: Dependencies
) {
// Delete any expired SnodeReceivedMessageInfo values associated to a specific node (even
// though this runs very quickly we fetch the rowIds we want to delete from a 'read' call
// to avoid blocking the write queue since this method is called very frequently)
let rowIds: [Int64] = Storage.shared
let rowIds: [Int64] = dependencies.storage
.read { db in
// Only prune the hashes if new hashes exist for this Snode (if they don't then
// we don't want to clear out the legacy hashes)
@ -102,7 +107,7 @@ public extension SnodeReceivedMessageInfo {
// If there are no rowIds to delete then do nothing
guard !rowIds.isEmpty else { return }
Storage.shared.write { db in
dependencies.storage.write { db in
try SnodeReceivedMessageInfo
.filter(rowIds.contains(Column.rowID))
.deleteAll(db)
@ -114,8 +119,13 @@ public extension SnodeReceivedMessageInfo {
/// **Note:** This method uses a `write` instead of a read because there is a single write queue for the database and it's
/// very common for this method to be called after the hash value has been updated but before the various `read` threads
/// have been updated, resulting in a pointless fetch for data the app has already received
static func fetchLastNotExpired(for snode: Snode, namespace: SnodeAPI.Namespace, associatedWith publicKey: String) -> SnodeReceivedMessageInfo? {
return Storage.shared.read { db in
static func fetchLastNotExpired(
for snode: Snode,
namespace: SnodeAPI.Namespace,
associatedWith publicKey: String,
using dependencies: Dependencies
) -> SnodeReceivedMessageInfo? {
return dependencies.storage.read { db in
let nonLegacyHash: SnodeReceivedMessageInfo? = try SnodeReceivedMessageInfo
.filter(
SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == nil ||

View File

@ -17,7 +17,7 @@ public enum GetSnodePoolJob: JobExecutor {
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
dependencies: Dependencies = Dependencies()
using dependencies: Dependencies
) {
// If we already have cached Snodes then we still want to trigger the 'SnodeAPI.getSnodePool'
// but we want to succeed this job immediately (since it's marked as blocking), this allows us
@ -35,7 +35,7 @@ public enum GetSnodePoolJob: JobExecutor {
// If we don't have the snode pool cached then we should also try to build the path (this will
// speed up the onboarding process for new users because it can run before the user is created)
SnodeAPI.getSnodePool()
.flatMap { _ in OnionRequestAPI.getPath(excluding: nil) }
.flatMap { _ in OnionRequestAPI.getPath(excluding: nil, using: dependencies) }
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(
@ -53,13 +53,14 @@ public enum GetSnodePoolJob: JobExecutor {
)
}
public static func run() {
public static func run(using dependencies: Dependencies = Dependencies()) {
GetSnodePoolJob.run(
Job(variant: .getSnodePool),
queue: .global(qos: .background),
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in }
deferred: { _, _ in },
using: dependencies
)
}
}

View File

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

View File

@ -6,23 +6,31 @@ import CryptoKit
import GRDB
import SessionUtilitiesKit
public protocol OnionRequestAPIType {
static func sendOnionRequest(_ payload: Data, to snode: Snode, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error>
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval) -> AnyPublisher<(ResponseInfoType, Data?), Error>
}
public extension OnionRequestAPIType {
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
return sendOnionRequest(payload, to: snode, timeout: HTTP.defaultTimeout)
public extension Network.RequestType {
static func onionRequest(_ payload: Data, to snode: Snode, timeout: TimeInterval = HTTP.defaultTimeout) -> Network.RequestType<Data?> {
return Network.RequestType(
id: "onionRequest",
url: snode.address,
method: "POST",
body: payload,
args: [payload, snode, timeout]
) { OnionRequestAPI.sendOnionRequest(payload, to: snode, timeout: timeout) }
}
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
return sendOnionRequest(request, to: server, with: x25519PublicKey, timeout: HTTP.defaultTimeout)
static func onionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval = HTTP.defaultTimeout) -> Network.RequestType<Data?> {
return Network.RequestType(
id: "onionRequest",
url: request.url?.absoluteString,
method: request.httpMethod,
headers: request.allHTTPHeaderFields,
body: request.httpBody,
args: [request, server, x25519PublicKey, timeout]
) { OnionRequestAPI.sendOnionRequest(request, to: server, with: x25519PublicKey, timeout: timeout) }
}
}
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI: OnionRequestAPIType {
public enum OnionRequestAPI {
private static var buildPathsPublisher: Atomic<AnyPublisher<[[Snode]], Error>?> = Atomic(nil)
private static var pathFailureCount: Atomic<[[Snode]: UInt]> = Atomic([:])
private static var snodeFailureCount: Atomic<[Snode: UInt]> = Atomic([:])
@ -66,12 +74,12 @@ public enum OnionRequestAPI: OnionRequestAPIType {
// 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) -> AnyPublisher<Void, Error> {
private static func testSnode(_ snode: Snode, using dependencies: Dependencies) -> AnyPublisher<Void, Error> {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
return HTTP.execute(.get, url, timeout: timeout)
.decoded(as: SnodeAPI.GetStatsResponse.self)
.decoded(as: SnodeAPI.GetStatsResponse.self, using: dependencies)
.tryMap { response -> Void in
guard let version: Version = response.version else { throw OnionRequestAPIError.missingSnodeVersion }
guard version >= Version(major: 2, minor: 0, patch: 7) else {
@ -86,7 +94,10 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// 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]) -> AnyPublisher<Set<Snode>, Error> {
private static func getGuardSnodes(
reusing reusableGuardSnodes: [Snode],
using dependencies: Dependencies
) -> AnyPublisher<Set<Snode>, Error> {
guard guardSnodes.wrappedValue.count < targetGuardSnodeCount else {
return Just(guardSnodes.wrappedValue)
.setFailureType(to: Error.self)
@ -115,7 +126,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
SNLog("Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate)
return testSnode(candidate, using: dependencies)
.map { _ in candidate }
.catch { _ in
return Just(())
@ -143,7 +154,10 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// 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: [[Snode]]) -> AnyPublisher<[[Snode]], Error> {
private static func buildPaths(
reusing reusablePaths: [[Snode]],
using dependencies: Dependencies
) -> AnyPublisher<[[Snode]], Error> {
if let existingBuildPathsPublisher = buildPathsPublisher.wrappedValue {
return existingBuildPathsPublisher
}
@ -164,7 +178,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// Need to include the post-request code and a `shareReplay` within the publisher otherwise it can still be executed
/// multiple times as a result of multiple subscribers
let reusableGuardSnodes = reusablePaths.map { $0[0] }
let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes)
let publisher: AnyPublisher<[[Snode]], Error> = getGuardSnodes(reusing: reusableGuardSnodes, using: dependencies)
.flatMap { (guardSnodes: Set<Snode>) -> AnyPublisher<[[Snode]], Error> in
var unusedSnodes: Set<Snode> = SnodeAPI.snodePool.wrappedValue
.subtracting(guardSnodes)
@ -227,7 +241,10 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
internal static func getPath(excluding snode: Snode?) -> AnyPublisher<[Snode], Error> {
internal static func getPath(
excluding snode: Snode?,
using dependencies: Dependencies
) -> AnyPublisher<[Snode], Error> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
let paths: [[Snode]] = OnionRequestAPI.paths
@ -257,7 +274,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
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
buildPaths(reusing: paths, using: dependencies) // Re-build paths in the background
.subscribe(on: DispatchQueue.global(qos: .background))
.sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in })
.store(in: &cancellable)
@ -267,7 +284,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.eraseToAnyPublisher()
}
else {
return buildPaths(reusing: paths)
return buildPaths(reusing: paths, using: dependencies)
.flatMap { paths in
guard let path: [Snode] = paths.filter({ !$0.contains(snode) }).randomElement() else {
return Fail<[Snode], Error>(error: OnionRequestAPIError.insufficientSnodes)
@ -282,7 +299,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
}
else {
buildPaths(reusing: paths) // Re-build paths in the background
buildPaths(reusing: paths, using: dependencies) // Re-build paths in the background
.subscribe(on: DispatchQueue.global(qos: .background))
.sink(receiveCompletion: { _ in cancellable = [] }, receiveValue: { _ in })
.store(in: &cancellable)
@ -298,7 +315,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
}
else {
return buildPaths(reusing: [])
return buildPaths(reusing: [], using: dependencies)
.flatMap { paths in
if let snode = snode {
if let path = paths.filter({ !$0.contains(snode) }).randomElement() {
@ -330,7 +347,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
private static func drop(_ snode: Snode) throws {
// We repair the path here because we can do it sync. In the case where we drop a whole
// path we leave the re-building up to getPath(excluding:) because re-building the path
// path we leave the re-building up to getPath(excluding:using:) because re-building the path
// in that case is async.
OnionRequestAPI.snodeFailureCount.mutate { $0[snode] = 0 }
var oldPaths = paths
@ -375,7 +392,8 @@ public enum OnionRequestAPI: OnionRequestAPIType {
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(
around payload: Data,
targetedAt destination: OnionRequestAPIDestination
targetedAt destination: OnionRequestAPIDestination,
using dependencies: Dependencies
) -> AnyPublisher<OnionBuildingResult, Error> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
@ -384,7 +402,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude)
return getPath(excluding: snodeToExclude, using: dependencies)
.flatMap { path -> AnyPublisher<AES.GCM.EncryptionResult, Error> in
guardSnode = path.first!
@ -490,11 +508,12 @@ public enum OnionRequestAPI: OnionRequestAPIType {
with payload: Data,
to destination: OnionRequestAPIDestination,
version: OnionRequestAPIVersion,
timeout: TimeInterval = HTTP.defaultTimeout
timeout: TimeInterval = HTTP.defaultTimeout,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
var guardSnode: Snode?
return buildOnion(around: payload, targetedAt: destination)
return buildOnion(around: payload, targetedAt: destination, using: dependencies)
.flatMap { intermediate -> AnyPublisher<(ResponseInfoType, Data?), Error> in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode!.address):\(guardSnode!.port)/onion_req/v2"

View File

@ -6,6 +6,18 @@ import Sodium
import GRDB
import SessionUtilitiesKit
public extension Network.RequestType {
static func message(
_ message: SnodeMessage,
in namespace: SnodeAPI.Namespace,
using dependencies: Dependencies = Dependencies()
) -> Network.RequestType<SendMessagesResponse> {
return Network.RequestType(id: "snodeAPI.sendMessage", args: [message, namespace]) {
SnodeAPI.sendMessage(message, in: namespace, using: dependencies)
}
}
}
public final class SnodeAPI {
internal static let sodium: Atomic<Sodium> = Atomic(Sodium())
@ -135,11 +147,13 @@ public final class SnodeAPI {
return !hasInsufficientSnodes
}
public static func getSnodePool() -> AnyPublisher<Set<Snode>, Error> {
public static func getSnodePool(
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Set<Snode>, Error> {
loadSnodePoolIfNeeded()
let now: Date = Date()
let hasSnodePoolExpired: Bool = Storage.shared[.lastSnodePoolRefreshDate]
let hasSnodePoolExpired: Bool = dependencies.storage[.lastSnodePoolRefreshDate]
.map { now.timeIntervalSince($0) > 2 * 60 * 60 }
.defaulting(to: true)
let snodePool: Set<Snode> = SnodeAPI.snodePool.wrappedValue
@ -163,10 +177,10 @@ public final class SnodeAPI {
}
let targetPublisher: AnyPublisher<Set<Snode>, Error> = {
guard snodePool.count >= minSnodePoolCount else { return getSnodePoolFromSeedNode() }
guard snodePool.count >= minSnodePoolCount else { return getSnodePoolFromSeedNode(using: dependencies) }
return getSnodePoolFromSnode()
.catch { _ in getSnodePoolFromSeedNode() }
return getSnodePoolFromSnode(using: dependencies)
.catch { _ in getSnodePoolFromSeedNode(using: dependencies) }
.eraseToAnyPublisher()
}()
@ -199,7 +213,10 @@ public final class SnodeAPI {
}
}
public static func getSessionID(for onsName: String) -> AnyPublisher<String, Error> {
public static func getSessionID(
for onsName: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<String, Error> {
let validationCount = 3
// The name must be lowercased
@ -236,7 +253,8 @@ public final class SnodeAPI {
)
),
to: snode,
associatedWith: nil
associatedWith: nil,
using: dependencies
)
.decoded(as: ONSResolveResponse.self)
.tryMap { _, response -> String in
@ -264,7 +282,7 @@ public final class SnodeAPI {
public static func getSwarm(
for publicKey: String,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Set<Snode>, Error> {
loadSwarmIfNeeded(for: publicKey)
@ -304,14 +322,14 @@ public final class SnodeAPI {
refreshingConfigHashes: [String] = [],
from snode: Snode,
associatedWith publicKey: String,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)], Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
.eraseToAnyPublisher()
}
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return Just(())
.setFailureType(to: Error.self)
@ -324,14 +342,16 @@ public final class SnodeAPI {
SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(
for: snode,
namespace: namespace,
associatedWith: publicKey
associatedWith: publicKey,
using: dependencies
)
result[namespace] = SnodeReceivedMessageInfo
.fetchLastNotExpired(
for: snode,
namespace: namespace,
associatedWith: publicKey
associatedWith: publicKey,
using: dependencies
)?
.hash
}
@ -445,7 +465,7 @@ public final class SnodeAPI {
.grouped(by: \.expiry)
.mapValues({ groupedResults in groupedResults.map { $0.hash } })
{
Storage.shared.writeAsync { db in
dependencies.storage.writeAsync { db in
try groupedExpiryResult.forEach { updatedExpiry, hashes in
try SnodeReceivedMessageInfo
.filter(hashes.contains(SnodeReceivedMessageInfo.Columns.hash))
@ -493,7 +513,7 @@ public final class SnodeAPI {
in namespace: SnodeAPI.Namespace,
from snode: Snode,
associatedWith publicKey: String,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?), Error> {
return Deferred {
Future<String?, Error> { resolver in
@ -501,14 +521,16 @@ public final class SnodeAPI {
SnodeReceivedMessageInfo.pruneExpiredMessageHashInfo(
for: snode,
namespace: namespace,
associatedWith: publicKey
associatedWith: publicKey,
using: dependencies
)
let maybeLastHash: String? = SnodeReceivedMessageInfo
.fetchLastNotExpired(
for: snode,
namespace: namespace,
associatedWith: publicKey
associatedWith: publicKey,
using: dependencies
)?
.hash
@ -592,7 +614,7 @@ public final class SnodeAPI {
public static func sendMessage(
_ message: SnodeMessage,
in namespace: Namespace,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, SendMessagesResponse), Error> {
let publicKey: String = message.recipient
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
@ -661,7 +683,7 @@ public final class SnodeAPI {
public static func sendConfigMessages(
_ messages: [(message: SnodeMessage, namespace: Namespace)],
allObsoleteHashes: [String],
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<HTTP.BatchResponse, Error> {
guard
!messages.isEmpty,
@ -755,7 +777,7 @@ public final class SnodeAPI {
publicKey: String,
serverHashes: [String],
updatedExpiryMs: UInt64,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: [(hash: String, expiry: UInt64)]], Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
@ -796,7 +818,7 @@ public final class SnodeAPI {
public static func revokeSubkey(
publicKey: String,
subkeyToRevoke: String,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
@ -839,14 +861,14 @@ public final class SnodeAPI {
public static func deleteMessages(
publicKey: String,
serverHashes: [String],
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
.eraseToAnyPublisher()
}
let userX25519PublicKey: String = getUserHexEncodedPublicKey()
let userX25519PublicKey: String = getUserHexEncodedPublicKey(using: dependencies)
return getSwarm(for: publicKey)
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<[String: Bool], Error> in
@ -894,7 +916,7 @@ public final class SnodeAPI {
/// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation.
public static func deleteAllMessages(
namespace: SnodeAPI.Namespace,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
@ -941,7 +963,7 @@ public final class SnodeAPI {
public static func deleteAllMessages(
beforeMs: UInt64,
namespace: SnodeAPI.Namespace,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<[String: Bool], Error> {
guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else {
return Fail(error: SnodeAPIError.noKeyPair)
@ -989,7 +1011,7 @@ public final class SnodeAPI {
private static func getNetworkTime(
from snode: Snode,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<UInt64, Error> {
return SnodeAPI
.send(
@ -998,7 +1020,8 @@ public final class SnodeAPI {
body: [:]
),
to: snode,
associatedWith: nil
associatedWith: nil,
using: dependencies
)
.decoded(as: GetNetworkTimestampResponse.self, using: dependencies)
.map { _, response in response.timestamp }
@ -1013,7 +1036,7 @@ public final class SnodeAPI {
}
private static func getSnodePoolFromSeedNode(
dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Set<Snode>, Error> {
let request: SnodeRequest = SnodeRequest(
endpoint: .jsonGetNServiceNodes,
@ -1073,7 +1096,7 @@ public final class SnodeAPI {
}
private static func getSnodePoolFromSnode(
dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<Set<Snode>, Error> {
var snodePool = SnodeAPI.snodePool.wrappedValue
var snodes: Set<Snode> = []
@ -1110,7 +1133,8 @@ public final class SnodeAPI {
)
),
to: snode,
associatedWith: nil
associatedWith: nil,
using: dependencies
)
.decoded(as: SnodePoolResponse.self, using: dependencies)
.mapError { error -> Error in
@ -1149,7 +1173,7 @@ public final class SnodeAPI {
request: SnodeRequest<T>,
to snode: Snode,
associatedWith publicKey: String?,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
guard let payload: Data = try? JSONEncoder().encode(request) else {
return Fail(error: HTTPError.invalidJSON)
@ -1175,11 +1199,8 @@ public final class SnodeAPI {
.eraseToAnyPublisher()
}
return dependencies.onionApi
.sendOnionRequest(
payload,
to: snode
)
return dependencies.network
.send(.onionRequest(payload, to: snode))
.mapError { error in
switch error {
case HTTPError.httpRequestFailed(let statusCode, let data):

View File

@ -1,38 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
open class SSKDependencies: Dependencies {
public var _onionApi: Atomic<OnionRequestAPIType.Type?>
public var onionApi: OnionRequestAPIType.Type {
get { Dependencies.getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } }
set { _onionApi.mutate { $0 = newValue } }
}
// MARK: - Initialization
public init(
subscribeQueue: DispatchQueue? = nil,
receiveQueue: DispatchQueue? = nil,
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: MutableGeneralCacheType? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_onionApi = Atomic(onionApi)
super.init(
subscribeQueue: subscribeQueue,
receiveQueue: receiveQueue,
generalCache: generalCache,
storage: storage,
scheduler: scheduler,
standardUserDefaults: standardUserDefaults,
date: date
)
}
}

Some files were not shown because too many files have changed in this diff Show More