Merge remote-tracking branch 'upstream/dev' into feature/updated-push-server

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	SessionMessagingKit/Open Groups/Types/SodiumProtocols.swift
#	SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift
#	SessionMessagingKitTests/_TestUtilities/MockAeadXChaCha20Poly1305Ietf.swift
#	SessionUtilitiesKit/JobRunner/JobRunner.swift
This commit is contained in:
Morgan Pretty 2023-08-02 14:35:16 +10:00
commit 18ee9d34fa
156 changed files with 9542 additions and 7473 deletions

View File

@ -13,7 +13,9 @@ local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https:/
// 'LANG' env var so we need to work around the with https://github.com/CocoaPods/CocoaPods/issues/6333
local install_cocoapods = {
name: 'Install CocoaPods',
commands: ['LANG=en_US.UTF-8 pod install']
commands: ['
LANG=en_US.UTF-8 pod install || rm -rf ./Pods && LANG=en_US.UTF-8 pod install
']
};
// Load from the cached CocoaPods directory (to speed up the build)
@ -21,8 +23,14 @@ local load_cocoapods_cache = {
name: 'Load CocoaPods Cache',
commands: [
|||
LOOP_BREAK=0
while test -e /Users/drone/.cocoapods_cache.lock; do
sleep 1
LOOP_BREAK=$((LOOP_BREAK + 1))
if [[ $LOOP_BREAK -ge 600 ]]; then
rm -f /Users/drone/.cocoapods_cache.lock
fi
done
|||,
'touch /Users/drone/.cocoapods_cache.lock',
@ -31,7 +39,7 @@ local load_cocoapods_cache = {
cp -r /Users/drone/.cocoapods_cache ./Pods
fi
|||,
'rm /Users/drone/.cocoapods_cache.lock'
'rm -f /Users/drone/.cocoapods_cache.lock'
]
};
@ -40,8 +48,14 @@ local update_cocoapods_cache = {
name: 'Update CocoaPods Cache',
commands: [
|||
LOOP_BREAK=0
while test -e /Users/drone/.cocoapods_cache.lock; do
sleep 1
LOOP_BREAK=$((LOOP_BREAK + 1))
if [[ $LOOP_BREAK -ge 600 ]]; then
rm -f /Users/drone/.cocoapods_cache.lock
fi
done
|||,
'touch /Users/drone/.cocoapods_cache.lock',
@ -51,7 +65,7 @@ local update_cocoapods_cache = {
cp -r ./Pods /Users/drone/.cocoapods_cache
fi
|||,
'rm /Users/drone/.cocoapods_cache.lock'
'rm -f /Users/drone/.cocoapods_cache.lock'
]
};
@ -71,7 +85,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
@ -83,6 +97,7 @@ local update_cocoapods_cache = {
type: 'exec',
name: 'Simulator Build',
platform: { os: 'darwin', arch: 'amd64' },
trigger: { event: { exclude: [ 'pull_request' ] } },
steps: [
clone_submodules,
load_cocoapods_cache,
@ -91,7 +106,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,
@ -110,6 +125,7 @@ local update_cocoapods_cache = {
type: 'exec',
name: 'AppStore Build',
platform: { os: 'darwin', arch: 'amd64' },
trigger: { event: { exclude: [ 'pull_request' ] } },
steps: [
clone_submodules,
load_cocoapods_cache,
@ -118,7 +134,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 | ./Pods/xcbeautify/xcbeautify --is-ci'
],
},
update_cocoapods_cache,

View File

@ -3,8 +3,6 @@
# Script used with Drone CI to upload build artifacts (because specifying all this in
# .drone.jsonnet is too painful).
set -o errexit
if [ -z "$SSH_KEY" ]; then
@ -19,33 +17,36 @@ set -o xtrace # Don't start tracing until *after* we write the ssh key
chmod 600 ssh_key
if [ -n "$DRONE_TAG" ]; then
# For a tag build use something like `session-ios-v1.2.3`
base="session-ios-$DRONE_TAG"
else
# Otherwise build a length name from the datetime and commit hash, such as:
# session-ios-20200522T212342Z-04d7dcc54
base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}"
fi
mkdir -v "$base"
# Copy over the build products
# Define the output paths
prod_path="build/Session.xcarchive"
sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app"
mkdir build
echo "Test" > "build/test.txt"
if [ ! -d $prod_path ]; then
cp -av $prod_path "$base"
else if [ ! -d $sim_path ]; then
cp -av $sim_path "$base"
# Validate the paths exist
if [ -d $prod_path ]; then
suffix="store"
target_path=$prod_path
elif [ -d $sim_path ]; then
suffix="sim"
target_path=$sim_path
else
echo "Expected a file to upload, found none" >&2
echo -e "\n\n\n\e[31;1mExpected a file to upload, found none\e[0m" >&2
exit 1
fi
if [ -n "$DRONE_TAG" ]; then
# For a tag build use something like `session-ios-v1.2.3`
base="session-ios-$DRONE_TAG-$suffix"
else
# Otherwise build a length name from the datetime and commit hash, such as:
# session-ios-20200522T212342Z-04d7dcc54
base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}-$suffix"
fi
# Copy over the build products
mkdir -vp "$base"
mkdir -p build
cp -av $target_path "$base"
# tar dat shiz up yo
archive="$base.tar.xz"
tar cJvf "$archive" "$base"

View File

@ -327,7 +327,6 @@
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; };
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA1255A581400E217F9 /* OWSOperation.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */; };
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC2255A581700E217F9 /* SSKAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; };
C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */; };
C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */; settings = {ATTRIBUTES = (Public, ); }; };
@ -471,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 */; };
@ -533,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 */; };
@ -616,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 */; };
@ -635,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 */; };
@ -698,7 +711,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; };
@ -710,6 +722,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 */; };
@ -718,11 +731,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 */; };
@ -740,11 +748,16 @@
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7432804EF1B004C14C5 /* JobRunner.swift */; };
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDE77F68280F9EDA002CFC5D /* JobRunnerError.swift */; };
FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */; };
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; };
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 */; };
@ -778,14 +791,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 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; };
@ -805,8 +815,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 */; };
@ -828,6 +836,7 @@
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 */; };
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 */; };
@ -862,7 +871,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 */; };
@ -924,6 +932,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 */
@ -1465,7 +1474,6 @@
C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLSessionDataTask+StatusCode.m"; sourceTree = "<group>"; };
C33FDBB6255A581600E217F9 /* DataSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DataSource.m; sourceTree = "<group>"; };
C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = "<group>"; };
C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = "<group>"; };
C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSSignalAddress.swift; sourceTree = "<group>"; };
C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignalIOS.pb.swift; sourceTree = "<group>"; };
C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushNotificationAPI.swift; sourceTree = "<group>"; };
@ -1640,10 +1648,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>"; };
@ -1699,6 +1704,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>"; };
@ -1750,9 +1764,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>"; };
@ -1768,6 +1781,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>"; };
@ -1826,7 +1840,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; };
@ -1837,6 +1850,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>"; };
@ -1848,11 +1862,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>"; };
@ -1867,10 +1876,13 @@
FD8ECF912938552800C0D1BB /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
FD8ECF93293856AF00C0D1BB /* Randomness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Randomness.swift; sourceTree = "<group>"; };
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSendJobSpec.swift; sourceTree = "<group>"; };
FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = "<group>"; };
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
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>"; };
@ -1904,13 +1916,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 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = "<group>"; };
@ -1932,8 +1941,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>"; };
@ -1954,6 +1961,7 @@
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>"; };
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>"; };
@ -1991,7 +1999,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>"; };
@ -2053,6 +2060,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 */
@ -2583,6 +2591,7 @@
FDC438B227BB15B400C60D73 /* ResponseInfo.swift */,
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */,
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
FD23CE1A2A651E6D0000B97C /* NetworkType.swift */,
FDF848EE294067E4007DCAE5 /* URLResponse+Utilities.swift */,
);
path = Networking;
@ -2610,14 +2619,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 */,
@ -3208,6 +3218,7 @@
C3A721332558BDDF0043A11F /* Open Groups */ = {
isa = PBXGroup;
children = (
FD23CE202A661CE80000B97C /* Crypto */,
FDC4381827B34EAD00C60D73 /* Models */,
FDC4380727B31D3A00C60D73 /* Types */,
FD3C907427E83AC200CD579F /* OpenGroupServerIdLookup.swift */,
@ -3232,9 +3243,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 */,
@ -3248,7 +3260,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 */,
@ -3267,7 +3278,6 @@
FDF8488C29405C04007DCAE5 /* Jobs */,
FDF8489229405C1B007DCAE5 /* Networking */,
C3C2A5CD255385F300C340D1 /* Utilities */,
FDF8488A29405BF2007DCAE5 /* SSKDependencies.swift */,
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
);
path = SessionSnodeKit;
@ -3334,7 +3344,6 @@
FD8ECF7529340F4800C0D1BB /* SessionUtil */,
FD3E0C82283B581F002A425C /* Shared Models */,
C3BBE0B32554F0D30050F1E3 /* Utilities */,
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */,
FD245C612850664300B966DD /* Configuration.swift */,
);
path = SessionMessagingKit;
@ -3414,7 +3423,6 @@
C33FDB8F255A581200E217F9 /* ParamParser.swift */,
C33FDADE255A580400E217F9 /* SwiftSingletons.swift */,
C33FDB49255A580C00E217F9 /* WeakTimer.swift */,
C33FDBC2255A581700E217F9 /* SSKAsserts.h */,
C33FDA9E255A57FF00E217F9 /* ReverseDispatchQueue.swift */,
C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */,
C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */,
@ -3582,7 +3590,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 */,
@ -3766,6 +3777,14 @@
path = Utilities;
sourceTree = "<group>";
};
FD23CE202A661CE80000B97C /* Crypto */ = {
isa = PBXGroup;
children = (
FD23CE212A661D000000B97C /* OpenGroupAPI+Crypto.swift */,
);
path = Crypto;
sourceTree = "<group>";
};
FD29598E2A43BE5400888A17 /* Utilities */ = {
isa = PBXGroup;
children = (
@ -3868,7 +3887,7 @@
FD3C906827E417B100CD579F /* Utilities */ = {
isa = PBXGroup;
children = (
FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */,
FD3C906927E417CE00CD579F /* CryptoSMKSpec.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -3906,6 +3925,7 @@
children = (
FD7115F628C8150D00B47552 /* Disposable Views */,
FD7115FD28C8202D00B47552 /* ReplaySubject.swift */,
FD83DCDC2A739D350065FFAE /* RetryWithDependencies.swift */,
FD7115FB28C8155800B47552 /* Publisher+Utilities.swift */,
FD71160128C8255900B47552 /* UIControl+Combine.swift */,
FD7115F928C8153400B47552 /* UIBarButtonItem+Combine.swift */,
@ -4030,6 +4050,7 @@
children = (
FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */,
FDDF074829DAB35200E5E8B5 /* JobRunner */,
FD9B30F1293EA0AF008DEE3E /* Networking */,
FDFBB7522A2023DE00CA7350 /* Utilities */,
FD29598E2A43BE5400888A17 /* Utilities */,
@ -4041,6 +4062,7 @@
isa = PBXGroup;
children = (
FD0B77B129B82B7A009169BA /* ArrayUtilitiesSpec.swift */,
FD23CE252A676B5B0000B97C /* DependenciesSpec.swift */,
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */,
);
path = General;
@ -4050,8 +4072,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 */,
@ -4128,6 +4156,22 @@
path = JobRunner;
sourceTree = "<group>";
};
FD96F3A229DBC3BA00401309 /* Jobs */ = {
isa = PBXGroup;
children = (
FD96F3A329DBC3D000401309 /* Types */,
);
path = Jobs;
sourceTree = "<group>";
};
FD96F3A329DBC3D000401309 /* Types */ = {
isa = PBXGroup;
children = (
FD96F3A429DBC3DC00401309 /* MessageSendJobSpec.swift */,
);
path = Types;
sourceTree = "<group>";
};
FD9B30F1293EA0AF008DEE3E /* Networking */ = {
isa = PBXGroup;
children = (
@ -4164,7 +4208,6 @@
FDC2909727D7129B005DAE71 /* PersonalizationSpec.swift */,
FDC2909327D710B4005DAE71 /* SOGSEndpointSpec.swift */,
FDC2909527D71252005DAE71 /* SOGSErrorSpec.swift */,
FDC2909B27D713D2005DAE71 /* SodiumProtocolsSpec.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4178,8 +4221,6 @@
FDC4380827B31D4E00C60D73 /* OpenGroupAPIError.swift */,
FDC4381627B32EC700C60D73 /* Personalization.swift */,
FD2959912A4417A900888A17 /* PreparedSendData.swift */,
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */,
FDC438C227BB512200C60D73 /* SodiumProtocols.swift */,
);
path = Types;
sourceTree = "<group>";
@ -4256,6 +4297,7 @@
FDC4389B27BA01E300C60D73 /* _TestUtilities */,
FD3C905D27E410DB00CD579F /* Common Networking */,
FD3C906527E416A200CD579F /* Contacts */,
FD96F3A229DBC3BA00401309 /* Jobs */,
FDC4389827BA001800C60D73 /* Open Groups */,
FD3C906B27E43C2400CD579F /* Sending & Receiving */,
FD7692F52A53A2C7000E4B70 /* Shared Models */,
@ -4280,19 +4322,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>";
@ -4305,6 +4336,14 @@
path = Utilities;
sourceTree = "<group>";
};
FDDF074829DAB35200E5E8B5 /* JobRunner */ = {
isa = PBXGroup;
children = (
FDDF074929DAB36900E5E8B5 /* JobRunnerSpec.swift */,
);
path = JobRunner;
sourceTree = "<group>";
};
FDE7214E287E50D50093DF33 /* Scripts */ = {
isa = PBXGroup;
children = (
@ -4459,7 +4498,6 @@
C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */,
C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */,
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */,
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */,
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */,
C33FDDBD255A582000E217F9 /* ByteParser.h in Headers */,
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */,
@ -4661,9 +4699,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 = (
@ -5587,7 +5625,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 */,
@ -5659,10 +5696,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 */,
@ -5721,8 +5761,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 */,
@ -5732,6 +5774,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 */,
@ -5785,7 +5828,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 */,
@ -5892,9 +5934,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 */,
@ -5925,7 +5967,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 */,
@ -5958,7 +6000,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 */,
@ -6184,13 +6225,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 */,
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;
@ -6203,14 +6249,21 @@
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FDFBB7542A2023EB00CA7350 /* BencodeSpec.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 */,
);
@ -6222,54 +6275,49 @@
files = (
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */,
FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */,
FD859EFA27C2F5C500510D0C /* MockGenericHash.swift in Sources */,
FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */,
FDC290B327DFF9F5005DAE71 /* TestOnionRequestAPI.swift in Sources */,
FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.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

@ -149,8 +149,14 @@ extension ConversationVC:
dismiss(animated: true, completion: nil)
}
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendMessage(text: (messageText ?? ""), attachments: attachments)
func sendMediaNav(
_ sendMediaNavigationController: SendMediaNavigationController,
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
@ -173,8 +179,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 +415,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 }
@ -480,20 +487,23 @@ extension ConversationVC:
quoteModel: quoteModel
)
sendMessage(optimisticData: optimisticData)
sendMessage(optimisticData: optimisticData, using: dependencies)
}
private func sendMessage(optimisticData: ConversationViewModel.OptimisticMessageData) {
private func sendMessage(
optimisticData: ConversationViewModel.OptimisticMessageData,
using dependencies: Dependencies
) {
let threadId: String = self.viewModel.threadData.threadId
let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant
DispatchQueue.global(qos:.userInitiated).async {
DispatchQueue.global(qos:.userInitiated).async(using: dependencies) {
// Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as
// this can take up to 0.5s
let quoteThumbnailAttachment: Attachment? = optimisticData.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 {
@ -541,7 +551,8 @@ extension ConversationVC:
db,
interaction: insertedInteraction,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
@ -798,10 +809,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
}
@ -875,8 +890,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,
@ -885,7 +900,9 @@ extension ConversationVC:
details: AttachmentDownloadJob.Details(
attachmentId: mediaView.attachment.id
)
)
),
canStartJob: true,
using: dependencies
)
}
break
@ -1024,8 +1041,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?) {
@ -1134,15 +1151,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
@ -1219,7 +1236,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 ||
@ -1237,7 +1254,7 @@ extension ConversationVC:
return
}
dependencies.mutableGeneralCache.mutate {
dependencies.caches.mutate(cache: .general) {
$0.recentReactionTimestamps = Array($0.recentReactionTimestamps
.suffix(19))
.appending(sentTimestamp)
@ -1272,9 +1289,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
@ -1383,7 +1400,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)
@ -1393,7 +1411,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)
@ -1444,14 +1462,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()
@ -1467,7 +1485,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() :
@ -1494,7 +1512,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
@ -1607,7 +1625,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),
@ -1618,8 +1636,7 @@ extension ConversationVC:
navigationController?.pushViewController(mediaInfoVC, animated: true)
}
func retry(_ cellViewModel: MessageViewModel) {
// If the failed message is an optimistic update then we need to do things differently
func retry(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
guard cellViewModel.id != MessageViewModel.optimisticUpdateId else {
guard
let optimisticMessageId: UUID = cellViewModel.optimisticMessageId,
@ -1640,11 +1657,11 @@ extension ConversationVC:
}
// Try to send the optimistic message again
self.sendMessage(optimisticData: optimisticMessageData)
sendMessage(optimisticData: optimisticMessageData, using: dependencies)
return
}
Storage.shared.writeAsync { [weak self] db in
dependencies.storage.writeAsync { [weak self] db in
guard
let threadId: String = self?.viewModel.threadData.threadId,
let threadVariant: SessionThread.Variant = self?.viewModel.threadData.threadVariant,
@ -1685,12 +1702,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,
@ -1713,7 +1731,7 @@ extension ConversationVC:
snInputView.becomeFirstResponder()
}
func copy(_ cellViewModel: MessageViewModel) {
func copy(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) {
switch cellViewModel.cellType {
case .typingIndicator, .dateHeader, .unreadMarker: break
@ -1751,7 +1769,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,
@ -1947,7 +1965,8 @@ extension ConversationVC:
message: unsendRequest,
threadId: cellViewModel.threadId,
interactionId: nil,
to: .contact(publicKey: userPublicKey)
to: .contact(publicKey: userPublicKey),
using: dependencies
)
}
return
@ -1970,7 +1989,8 @@ extension ConversationVC:
message: unsendRequest,
threadId: cellViewModel.threadId,
interactionId: nil,
to: .contact(publicKey: userPublicKey)
to: .contact(publicKey: userPublicKey),
using: dependencies
)
}
self?.showInputAccessoryView()
@ -1998,7 +2018,8 @@ extension ConversationVC:
message: unsendRequest,
interactionId: nil,
threadId: cellViewModel.threadId,
threadVariant: cellViewModel.threadVariant
threadVariant: cellViewModel.threadVariant,
using: dependencies
)
}
@ -2032,7 +2053,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 ?? [])
@ -2074,24 +2095,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
@ -2147,7 +2154,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
@ -2205,7 +2212,7 @@ extension ConversationVC:
// MARK: - VoiceMessageRecordingViewDelegate
func startVoiceMessageRecording() {
func startVoiceMessageRecording(using dependencies: Dependencies) {
// Request permission if needed
Permissions.requestMicrophonePermissionIfNeeded() { [weak self] in
DispatchQueue.main.async {
@ -2254,7 +2261,7 @@ extension ConversationVC:
// Limit voice messages to a minute
audioTimer = Timer.scheduledTimer(withTimeInterval: 180, repeats: false, block: { [weak self] _ in
self?.snInputView.hideVoiceMessageUI()
self?.endVoiceMessageRecording()
self?.endVoiceMessageRecording(using: dependencies)
})
// Prepare audio recorder
@ -2270,7 +2277,7 @@ extension ConversationVC:
}
}
func endVoiceMessageRecording() {
func endVoiceMessageRecording(using dependencies: Dependencies) {
UIApplication.shared.isIdleTimerDisabled = true
// Hide the UI
@ -2322,7 +2329,7 @@ extension ConversationVC:
}
// Send attachment
sendMessage(text: "", attachments: [attachment])
sendMessage(text: "", attachments: [attachment], using: dependencies)
}
func cancelVoiceMessageRecording() {
@ -2339,23 +2346,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
)
}
}
@ -2391,7 +2404,8 @@ extension ConversationVC {
for threadId: String,
threadVariant: SessionThread.Variant,
isNewThread: Bool,
timestampMs: Int64
timestampMs: Int64,
using dependencies: Dependencies = Dependencies()
) {
guard threadVariant == .contact else { return }
@ -2432,7 +2446,8 @@ extension ConversationVC {
),
interactionId: nil,
threadId: threadId,
threadVariant: threadVariant
threadVariant: threadVariant,
using: dependencies
)
}

View File

@ -416,14 +416,14 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
if inputViewButton == sendButton { delegate?.handleSendButtonTapped() }
}
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) {
guard inputViewButton == voiceMessageButton else { return }
// Note: The 'showVoiceMessageUI' call MUST come before triggering 'startVoiceMessageRecording'
// because if something goes wrong it'll trigger `hideVoiceMessageUI` and we don't want it to
// end up in a state with the input content hidden
showVoiceMessageUI()
delegate?.startVoiceMessageRecording()
delegate?.startVoiceMessageRecording(using: dependencies)
}
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) {

View File

@ -137,7 +137,9 @@ final class InputViewButton: UIView {
// We want to detect both taps and long presses
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { onTouchesBegan() }
private func onTouchesBegan(using dependencies: Dependencies = Dependencies()) {
guard isUserInteractionEnabled else { return }
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
@ -145,7 +147,7 @@ final class InputViewButton: UIView {
invalidateLongPressIfNeeded()
longPressTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false, block: { [weak self] _ in
self?.isLongPress = true
self?.delegate?.handleInputViewButtonLongPressBegan(self)
self?.delegate?.handleInputViewButtonLongPressBegan(self, using: dependencies)
})
}
@ -185,13 +187,13 @@ final class InputViewButton: UIView {
protocol InputViewButtonDelegate: AnyObject {
func handleInputViewButtonTapped(_ inputViewButton: InputViewButton)
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?)
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies)
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?)
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?)
}
extension InputViewButtonDelegate {
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?) { }
func handleInputViewButtonLongPressBegan(_ inputViewButton: InputViewButton?, using dependencies: Dependencies) { }
func handleInputViewButtonLongPressMoved(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
func handleInputViewButtonLongPressEnded(_ inputViewButton: InputViewButton, with touch: UITouch?) { }
}

View File

@ -310,12 +310,12 @@ final class VoiceMessageRecordingView: UIView {
}
}
func handleLongPressEnded(at location: CGPoint) {
func handleLongPressEnded(at location: CGPoint, using dependencies: Dependencies = Dependencies()) {
if pulseView.frame.contains(location) {
delegate?.endVoiceMessageRecording()
delegate?.endVoiceMessageRecording(using: dependencies)
}
else if isValidLockViewLocation(location) {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleCircleViewTap))
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(onCircleViewTap))
circleView.addGestureRecognizer(tapGestureRecognizer)
UIView.animate(withDuration: 0.25, delay: 0, options: .transitionCrossDissolve, animations: {
@ -332,8 +332,10 @@ final class VoiceMessageRecordingView: UIView {
}
}
@objc private func handleCircleViewTap() {
delegate?.endVoiceMessageRecording()
@objc private func onCircleViewTap() { handleCircleViewTap() }
private func handleCircleViewTap(using dependencies: Dependencies = Dependencies()) {
delegate?.endVoiceMessageRecording(using: dependencies)
}
@objc private func handleCancelButtonTapped() {
@ -474,7 +476,7 @@ extension VoiceMessageRecordingView {
// MARK: - Delegate
protocol VoiceMessageRecordingViewDelegate: AnyObject {
func startVoiceMessageRecording()
func endVoiceMessageRecording()
func startVoiceMessageRecording(using dependencies: Dependencies)
func endVoiceMessageRecording(using dependencies: Dependencies)
func cancelVoiceMessageRecording()
}

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

@ -430,8 +430,8 @@ extension SendMediaNavigationController: AttachmentApprovalViewControllerDelegat
attachmentDraftCollection.remove(attachment: attachment)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText)
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies) {
sendMediaNavDelegate?.sendMediaNav(self, didApproveAttachments: attachments, forThreadId: threadId, messageText: messageText, using: dependencies)
}
func attachmentApprovalDidCancel(_ attachmentApproval: AttachmentApprovalViewController) {
@ -764,7 +764,7 @@ private class DoneButton: UIView {
protocol SendMediaNavDelegate: AnyObject {
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?)
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?, using dependencies: Dependencies)
func sendMediaNavInitialMessageText(_ sendMediaNavigationController: SendMediaNavigationController) -> String?
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didChangeMessageText newMessageText: String?)

View File

@ -92,7 +92,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
// No point continuing if we are running tests
guard !CurrentAppContext().isRunningTests else { return true }
guard !SNUtilitiesKit.isRunningTests else { return true }
self.window = mainWindow
CurrentAppContext().mainWindow = mainWindow
@ -212,7 +212,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}
func applicationDidBecomeActive(_ application: UIApplication) {
guard !CurrentAppContext().isRunningTests else { return }
guard !SNUtilitiesKit.isRunningTests else { return }
UserDefaults.sharedLokiProject?[.isMainAppActive] = true
@ -314,7 +314,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
private func completePostMigrationSetup(calledFrom lifecycleMethod: LifecycleMethod, needsConfigSync: Bool) {
SNLog("Migrations completed, performing setup and ensuring rootViewController")
Configuration.performMainSetup()
JobRunner.add(executor: SyncPushTokensJob.self, for: .syncPushTokens)
JobRunner.setExecutor(SyncPushTokensJob.self, for: .syncPushTokens)
// Setup the UI if needed, then trigger any post-UI setup actions
self.ensureRootViewController(calledFrom: lifecycleMethod) { [weak self] success in

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
import SignalUtilitiesKit
import SignalCoreKit
@ -11,7 +12,7 @@ public class AppEnvironment {
public class var shared: AppEnvironment {
get { return _shared }
set {
guard CurrentAppContext().isRunningTests else {
guard SNUtilitiesKit.isRunningTests else {
owsFailDebug("Can only switch environments in tests.")
return
}

View File

@ -61,8 +61,6 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
name:UIApplicationWillTerminateNotification
object:nil];
// We can't use OWSSingletonAssert() since it uses the app context.
self.appActiveBlocks = [NSMutableArray new];
return self;
@ -243,11 +241,6 @@ NSString *const ReportedApplicationStateDidChangeNotification = @"ReportedApplic
}];
}
- (BOOL)isRunningTests
{
return (NSProcessInfo.processInfo.environment[@"XCTestConfigurationFilePath"] != nil);
}
- (void)setNetworkActivityIndicatorVisible:(BOOL)value
{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:value];

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

@ -17,17 +17,18 @@ public enum SyncPushTokensJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, 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 {
return deferred(job) // Don't need to do anything if it's not the main app
return deferred(job, dependencies) // Don't need to do anything if it's not the main app
}
guard Identity.userCompletedRequiredOnboarding() else {
SNLog("[SyncPushTokensJob] Deferred due to incomplete registration")
return deferred(job)
return deferred(job, dependencies)
}
// We need to check a UIApplication setting which needs to run on the main thread so synchronously
@ -46,7 +47,7 @@ public enum SyncPushTokensJob: JobExecutor {
// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/HandlingRemoteNotifications.html#//apple_ref/doc/uid/TP40008194-CH6-SW1
guard job.behaviour == .runOnce || !isRegisteredForRemoteNotifications else {
SNLog("[SyncPushTokensJob] Deferred due to Fast Mode disabled")
deferred(job) // Don't need to do anything if push notifications are already registered
deferred(job, dependencies) // Don't need to do anything if push notifications are already registered
return
}
@ -56,7 +57,7 @@ public enum SyncPushTokensJob: JobExecutor {
// If the job is running and 'Fast Mode' is disabled then we should try to unregister the existing
// token
guard isUsingFullAPNs else {
Just(Storage.shared[.lastRecordedPushToken])
Just(dependencies.storage[.lastRecordedPushToken])
.setFailureType(to: Error.self)
.flatMap { lastRecordedPushToken -> AnyPublisher<String, Error> in
if let existingToken: String = lastRecordedPushToken {
@ -77,12 +78,12 @@ public enum SyncPushTokensJob: JobExecutor {
// the token if needed
DispatchQueue.main.sync { UIApplication.shared.unregisterForRemoteNotifications() }
Storage.shared.write { db in
dependencies.storage.write(using: dependencies) { db in
db[.lastRecordedPushToken] = nil
}
return ()
}
.subscribe(on: queue)
.subscribe(on: queue, using: dependencies)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
@ -91,7 +92,7 @@ public enum SyncPushTokensJob: JobExecutor {
}
// We want to complete this job regardless of success or failure
success(job, false)
success(job, false, dependencies)
}
)
return
@ -104,9 +105,10 @@ public enum SyncPushTokensJob: JobExecutor {
PushNotificationAPI
.subscribe(
token: Data(hex: pushToken),
isForcedUpdate: true
isForcedUpdate: true,
using: dependencies
)
.retry(3)
.retry(3, using: dependencies)
.handleEvents(
receiveCompletion: { result in
switch result {
@ -116,9 +118,9 @@ public enum SyncPushTokensJob: JobExecutor {
case .finished:
Logger.warn("Recording push tokens locally. pushToken: \(redact(pushToken)), voipToken: \(redact(voipToken))")
SNLog("[SyncPushTokensJob] Completed")
UserDefaults.standard[.lastPushNotificationSync] = Date()
dependencies.standardUserDefaults[.lastPushNotificationSync] = dependencies.dateNow
Storage.shared.write { db in
dependencies.storage.write(using: dependencies) { db in
db[.lastRecordedPushToken] = pushToken
db[.lastRecordedVoipToken] = voipToken
}
@ -128,10 +130,10 @@ public enum SyncPushTokensJob: JobExecutor {
.map { _ in () }
.eraseToAnyPublisher()
}
.subscribe(on: queue)
.subscribe(on: queue, using: dependencies)
.sinkUntilComplete(
// We want to complete this job regardless of success or failure
receiveCompletion: { _ in success(job, false) }
receiveCompletion: { _ in success(job, false, dependencies) }
)
}
@ -148,9 +150,9 @@ public enum SyncPushTokensJob: JobExecutor {
SyncPushTokensJob.run(
job,
queue: DispatchQueue.global(qos: .default),
success: { _, _ in },
failure: { _, _, _ in },
deferred: { _ in }
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in }
)
}
}

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

@ -245,7 +245,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

@ -45,20 +45,20 @@ public enum SNMessagingKit: MigratableTarget { // Just to make the external API
public static func configure() {
// Configure the job executors
JobRunner.add(executor: DisappearingMessagesJob.self, for: .disappearingMessages)
JobRunner.add(executor: FailedMessageSendsJob.self, for: .failedMessageSends)
JobRunner.add(executor: FailedAttachmentDownloadsJob.self, for: .failedAttachmentDownloads)
JobRunner.add(executor: UpdateProfilePictureJob.self, for: .updateProfilePicture)
JobRunner.add(executor: RetrieveDefaultOpenGroupRoomsJob.self, for: .retrieveDefaultOpenGroupRooms)
JobRunner.add(executor: GarbageCollectionJob.self, for: .garbageCollection)
JobRunner.add(executor: MessageSendJob.self, for: .messageSend)
JobRunner.add(executor: MessageReceiveJob.self, for: .messageReceive)
JobRunner.add(executor: NotifyPushServerJob.self, for: .notifyPushServer)
JobRunner.add(executor: SendReadReceiptsJob.self, for: .sendReadReceipts)
JobRunner.add(executor: AttachmentUploadJob.self, for: .attachmentUpload)
JobRunner.add(executor: GroupLeavingJob.self, for: .groupLeaving)
JobRunner.add(executor: AttachmentDownloadJob.self, for: .attachmentDownload)
JobRunner.add(executor: ConfigurationSyncJob.self, for: .configurationSync)
JobRunner.add(executor: ConfigMessageReceiveJob.self, for: .configMessageReceive)
JobRunner.setExecutor(DisappearingMessagesJob.self, for: .disappearingMessages)
JobRunner.setExecutor(FailedMessageSendsJob.self, for: .failedMessageSends)
JobRunner.setExecutor(FailedAttachmentDownloadsJob.self, for: .failedAttachmentDownloads)
JobRunner.setExecutor(UpdateProfilePictureJob.self, for: .updateProfilePicture)
JobRunner.setExecutor(RetrieveDefaultOpenGroupRoomsJob.self, for: .retrieveDefaultOpenGroupRooms)
JobRunner.setExecutor(GarbageCollectionJob.self, for: .garbageCollection)
JobRunner.setExecutor(MessageSendJob.self, for: .messageSend)
JobRunner.setExecutor(MessageReceiveJob.self, for: .messageReceive)
JobRunner.setExecutor(NotifyPushServerJob.self, for: .notifyPushServer)
JobRunner.setExecutor(SendReadReceiptsJob.self, for: .sendReadReceipts)
JobRunner.setExecutor(AttachmentUploadJob.self, for: .attachmentUpload)
JobRunner.setExecutor(GroupLeavingJob.self, for: .groupLeaving)
JobRunner.setExecutor(AttachmentDownloadJob.self, for: .attachmentDownload)
JobRunner.setExecutor(ConfigurationSyncJob.self, for: .configurationSync)
JobRunner.setExecutor(ConfigMessageReceiveJob.self, for: .configMessageReceive)
}
}

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

@ -163,7 +163,7 @@ internal extension ControlMessageProcessRecord {
.infoClosedGroupCreated:
return nil
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
case .infoClosedGroupUpdated, .infoClosedGroupCurrentUserLeft, .infoClosedGroupCurrentUserLeaving, .infoClosedGroupCurrentUserErrorLeaving:
self.variant = .closedGroupControlMessage
case .infoDisappearingMessagesUpdate:

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,11 +250,11 @@ 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) -> Profile {
static func fetchOrCreateCurrentUser(_ db: Database? = nil, using dependencies: Dependencies = Dependencies()) -> Profile {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
guard let db: Database = db else {
return Storage.shared
return dependencies.storage
.read { db in fetchOrCreateCurrentUser(db) }
.defaulting(to: defaultFor(userPublicKey))
}

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

@ -14,9 +14,10 @@ public enum AttachmentDownloadJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
@ -25,7 +26,7 @@ public enum AttachmentDownloadJob: JobExecutor {
let attachment: Attachment = Storage.shared
.read({ db in try Attachment.fetchOne(db, id: details.attachmentId) })
else {
failure(job, JobRunnerError.missingRequiredDetails, true)
failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
return
}
@ -33,7 +34,7 @@ public enum AttachmentDownloadJob: JobExecutor {
// an AttachmentDownloadJob to get created for an attachment which has already been
// downloaded/uploaded so in those cases just succeed immediately
guard attachment.state != .downloaded && attachment.state != .uploaded else {
success(job, false)
success(job, false, dependencies)
return
}
@ -41,8 +42,8 @@ public enum AttachmentDownloadJob: JobExecutor {
// the same attachment multiple times at the same time (it also adds a "clean up" mechanism
// if an attachment ends up stuck in a "downloading" state incorrectly
guard attachment.state != .downloading else {
let otherCurrentJobAttachmentIds: Set<String> = JobRunner
.infoForCurrentlyRunningJobs(of: .attachmentDownload)
let otherCurrentJobAttachmentIds: Set<String> = dependencies.jobRunner
.jobInfoFor(state: .running, variant: .attachmentDownload)
.filter { key, _ in key != job.id }
.values
.compactMap { info -> String? in
@ -57,7 +58,7 @@ public enum AttachmentDownloadJob: JobExecutor {
// then we should update the state of the attachment to be failed to avoid having attachments
// appear in an endlessly downloading state
if !otherCurrentJobAttachmentIds.contains(attachment.id) {
Storage.shared.write { db in
dependencies.storage.write { db in
_ = try Attachment
.filter(id: attachment.id)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedDownload))
@ -70,12 +71,12 @@ public enum AttachmentDownloadJob: JobExecutor {
// If there is another current job then just fail this one permanently, otherwise let it
// retry (if there are more retry attempts available) and in the next retry it's state should
// be 'failedDownload' so we won't get stuck in a loop
failure(job, nil, otherCurrentJobAttachmentIds.contains(attachment.id))
failure(job, nil, otherCurrentJobAttachmentIds.contains(attachment.id), dependencies)
return
}
// Update to the 'downloading' state (no need to update the 'attachment' instance)
Storage.shared.write { db in
dependencies.storage.write { db in
try Attachment
.filter(id: attachment.id)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.downloading))
@ -155,16 +156,16 @@ public enum AttachmentDownloadJob: JobExecutor {
}
.sinkUntilComplete(
receiveCompletion: { result in
// Remove the temporary file
OWSFileSystem.deleteFile(temporaryFileUrl.path)
switch result {
case .finished:
// Remove the temporary file
OWSFileSystem.deleteFile(temporaryFileUrl.path)
/// Update the attachment state
///
/// **Note:** We **MUST** use the `'with()` function here as it will update the
/// `isValid` and `duration` values based on the downloaded data and the state
Storage.shared.write { db in
dependencies.storage.write { db in
_ = try attachment
.with(
state: .downloaded,
@ -177,11 +178,9 @@ public enum AttachmentDownloadJob: JobExecutor {
.saved(db)
}
success(job, false)
success(job, false, dependencies)
case .failure(let error):
OWSFileSystem.deleteFile(temporaryFileUrl.path)
let targetState: Attachment.State
let permanentFailure: Bool
@ -211,14 +210,14 @@ public enum AttachmentDownloadJob: JobExecutor {
///
/// **Note:** We **MUST** use the `'with()` function here as it will update the
/// `isValid` and `duration` values based on the downloaded data and the state
Storage.shared.write { db in
dependencies.storage.write { db in
_ = try Attachment
.filter(id: attachment.id)
.updateAll(db, Attachment.Columns.state.set(to: targetState))
}
/// Trigger the failure and provide the `permanentFailure` value defined above
failure(job, error, permanentFailure)
failure(job, error, permanentFailure, dependencies)
}
}
)

View File

@ -14,16 +14,17 @@ public enum AttachmentUploadJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
let interactionId: Int64 = job.interactionId,
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
let (attachment, openGroup): (Attachment, OpenGroup?) = Storage.shared.read({ db in
let (attachment, openGroup): (Attachment, OpenGroup?) = dependencies.storage.read({ db in
guard let attachment: Attachment = try Attachment.fetchOne(db, id: details.attachmentId) else {
return nil
}
@ -32,29 +33,26 @@ public enum AttachmentUploadJob: JobExecutor {
})
else {
SNLog("[AttachmentUploadJob] Failed due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// If the original interaction no longer exists then don't bother uploading the attachment (ie. the
// message was deleted before it even got sent)
guard Storage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true else {
guard dependencies.storage.read({ db in try Interaction.exists(db, id: interactionId) }) == true else {
SNLog("[AttachmentUploadJob] Failed due to missing interaction")
failure(job, StorageError.objectNotFound, true)
return
return failure(job, StorageError.objectNotFound, true, dependencies)
}
// If the attachment is still pending download the hold off on running this job
guard attachment.state != .pendingDownload && attachment.state != .downloading else {
SNLog("[AttachmentUploadJob] Deferred as attachment is still being downloaded")
deferred(job)
return
return deferred(job, dependencies)
}
// If this upload is related to sending a message then trigger the 'handleMessageWillSend' logic
// as if this is a retry the logic wouldn't run until after the upload has completed resulting in
// a potentially incorrect delivery status
Storage.shared.write { db in
dependencies.storage.write { db in
guard
let sendJob: Job = try Job.fetchOne(db, id: details.messageSendJobId),
let sendJobDetails: Data = sendJob.details,
@ -74,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(
@ -84,7 +82,7 @@ public enum AttachmentUploadJob: JobExecutor {
// If this upload is related to sending a message then trigger the
// 'handleFailedMessageSend' logic as we want to ensure the message
// has the correct delivery status
Storage.shared.read { db in
dependencies.storage.read { db in
guard
let sendJob: Job = try Job.fetchOne(db, id: details.messageSendJobId),
let sendJobDetails: Data = sendJob.details,
@ -97,14 +95,15 @@ public enum AttachmentUploadJob: JobExecutor {
message: details.message,
with: .other(error),
interactionId: interactionId,
isSyncMessage: details.isSyncMessage
isSyncMessage: details.isSyncMessage,
using: dependencies
)
}
SNLog("[AttachmentUploadJob] Failed due to error: \(error)")
failure(job, error, false)
failure(job, error, false, dependencies)
case .finished: success(job, false)
case .finished: success(job, false, dependencies)
}
}
)

View File

@ -12,9 +12,10 @@ public enum ConfigMessageReceiveJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, 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
@ -24,7 +25,7 @@ public enum ConfigMessageReceiveJob: JobExecutor {
let removeDependencyOnMessageReceiveJobs: () -> () = {
guard let jobId: Int64 = job.id else { return }
Storage.shared.write { db in
dependencies.storage.write { db in
try JobDependencies
.filter(JobDependencies.Columns.dependantId == jobId)
.joining(
@ -40,23 +41,21 @@ public enum ConfigMessageReceiveJob: JobExecutor {
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
removeDependencyOnMessageReceiveJobs()
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// Ensure no standard messages are sent through this job
guard !details.messages.contains(where: { $0.variant != .sharedConfigMessage }) else {
SNLog("[ConfigMessageReceiveJob] Standard messages incorrectly sent to the 'configMessageReceive' job")
removeDependencyOnMessageReceiveJobs()
failure(job, MessageReceiverError.invalidMessage, true)
return
return failure(job, MessageReceiverError.invalidMessage, true, dependencies)
}
var lastError: Error?
let sharedConfigMessages: [SharedConfigMessage] = details.messages
.compactMap { $0.message as? SharedConfigMessage }
Storage.shared.write { db in
dependencies.storage.write { db in
// Send any SharedConfigMessages to the SessionUtil to handle it
do {
try SessionUtil.handleConfigMessages(
@ -72,9 +71,9 @@ public enum ConfigMessageReceiveJob: JobExecutor {
switch lastError {
case .some(let error):
removeDependencyOnMessageReceiveJobs()
failure(job, error, true)
failure(job, error, true, dependencies)
case .none: success(job, false)
case .none: success(job, false, dependencies)
}
}
}

View File

@ -15,22 +15,24 @@ public enum ConfigurationSyncJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
SessionUtil.userConfigsEnabled,
Identity.userCompletedRequiredOnboarding()
else { return success(job, true) }
else { return success(job, true, dependencies) }
// It's possible for multiple ConfigSyncJob's with the same target (user/group) to try to run at the
// same time since as soon as one is started we will enqueue a second one, rather than adding dependencies
// between the jobs we just continue to defer the subsequent job while the first one is running in
// order to prevent multiple configurationSync jobs with the same target from running at the same time
guard
JobRunner
.infoForCurrentlyRunningJobs(of: .configurationSync)
dependencies
.jobRunner
.jobInfoFor(state: .running, variant: .configurationSync)
.filter({ key, info in
key != job.id && // Exclude this job
info.threadId == job.threadId // Exclude jobs for different ids
@ -39,14 +41,14 @@ public enum ConfigurationSyncJob: JobExecutor {
else {
// Defer the job to run 'maxRunFrequency' from when this one ran (if we don't it'll try start
// it again immediately which is pointless)
let updatedJob: Job? = Storage.shared.write { db in
let updatedJob: Job? = dependencies.storage.write { db in
try job
.with(nextRunTimestamp: Date().timeIntervalSince1970 + maxRunFrequency)
.with(nextRunTimestamp: dependencies.dateNow.timeIntervalSince1970 + maxRunFrequency)
.saved(db)
}
SNLog("[ConfigurationSyncJob] For \(job.threadId ?? "UnknownId") deferred due to in progress job")
return deferred(updatedJob ?? job)
return deferred(updatedJob ?? job, dependencies)
}
// If we don't have a userKeyPair yet then there is no need to sync the configuration
@ -58,14 +60,14 @@ public enum ConfigurationSyncJob: JobExecutor {
.read({ db in try SessionUtil.pendingChanges(db, publicKey: publicKey) })
else {
SNLog("[ConfigurationSyncJob] For \(job.threadId ?? "UnknownId") failed due to invalid data")
return failure(job, StorageError.generic, false)
return failure(job, StorageError.generic, false, dependencies)
}
// If there are no pending changes then the job can just complete (next time something
// is updated we want to try and run immediately so don't scuedule another run in this case)
guard !pendingConfigChanges.isEmpty else {
SNLog("[ConfigurationSyncJob] For \(publicKey) completed with no pending changes")
return success(job, true)
return success(job, true, dependencies)
}
// Identify the destination and merge all obsolete hashes into a single set
@ -77,10 +79,10 @@ 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")")
Storage.shared
dependencies.storage
.readPublisher { db in
try pendingConfigChanges.map { change -> MessageSender.PreparedSendData in
try MessageSender.preparedSendData(
@ -103,7 +105,8 @@ public enum ConfigurationSyncJob: JobExecutor {
return (snodeMessage, namespace)
},
allObsoleteHashes: Array(allObsoleteHashes)
allObsoleteHashes: Array(allObsoleteHashes),
using: dependencies
)
}
.subscribe(on: queue)
@ -138,7 +141,7 @@ public enum ConfigurationSyncJob: JobExecutor {
case .finished: SNLog("[ConfigurationSyncJob] For \(publicKey) completed")
case .failure(let error):
SNLog("[ConfigurationSyncJob] For \(publicKey) failed due to error: \(error)")
failure(job, error, false)
failure(job, error, false, dependencies)
}
},
receiveValue: { (configDumps: [ConfigDump]) in
@ -146,7 +149,7 @@ public enum ConfigurationSyncJob: JobExecutor {
var shouldFinishCurrentJob: Bool = false
// Lastly we need to save the updated dumps to the database
let updatedJob: Job? = Storage.shared.write { db in
let updatedJob: Job? = dependencies.storage.write { db in
// Save the updated dumps to the database
try configDumps.forEach { try $0.save(db) }
@ -167,7 +170,7 @@ public enum ConfigurationSyncJob: JobExecutor {
{
// If the next job isn't currently running then delay it's start time
// until the 'nextRunTimestamp'
if !JobRunner.isCurrentlyRunning(existingJob) {
if !dependencies.jobRunner.isCurrentlyRunning(existingJob) {
_ = try existingJob
.with(nextRunTimestamp: nextRunTimestamp)
.saved(db)
@ -183,7 +186,7 @@ public enum ConfigurationSyncJob: JobExecutor {
.saved(db)
}
success((updatedJob ?? job), shouldFinishCurrentJob)
success((updatedJob ?? job), shouldFinishCurrentJob, dependencies)
}
)
}
@ -192,7 +195,11 @@ public enum ConfigurationSyncJob: JobExecutor {
// MARK: - Convenience
public extension ConfigurationSyncJob {
static func enqueue(_ db: Database, publicKey: String) {
static func enqueue(
_ db: Database,
publicKey: String,
dependencies: Dependencies = Dependencies()
) {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard SessionUtil.userConfigsEnabled(db) else {
// If we don't have a userKeyPair (or name) yet then there is no need to sync the
@ -206,7 +213,7 @@ public extension ConfigurationSyncJob {
let publicKey: String = getUserHexEncodedPublicKey(db)
JobRunner.add(
dependencies.jobRunner.add(
db,
job: Job(
variant: .messageSend,
@ -215,26 +222,34 @@ public extension ConfigurationSyncJob {
destination: Message.Destination.contact(publicKey: publicKey),
message: legacyConfigMessage
)
)
),
canStartJob: true,
using: dependencies
)
return
}
// Upsert a config sync job if needed
JobRunner.upsert(
dependencies.jobRunner.upsert(
db,
job: ConfigurationSyncJob.createIfNeeded(db, publicKey: publicKey)
job: ConfigurationSyncJob.createIfNeeded(db, publicKey: publicKey, using: dependencies),
canStartJob: true,
using: dependencies
)
}
@discardableResult static func createIfNeeded(_ db: Database, publicKey: String) -> Job? {
@discardableResult static func createIfNeeded(
_ db: Database,
publicKey: String,
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
///
/// **Note:** Jobs with different `threadId` values can run concurrently
guard
JobRunner
.infoForCurrentlyRunningJobs(of: .configurationSync)
dependencies.jobRunner
.jobInfoFor(state: .running, variant: .configurationSync)
.filter({ _, info in info.threadId == publicKey })
.isEmpty,
(try? Job
@ -252,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
@ -262,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()
}
@ -282,9 +298,10 @@ public extension ConfigurationSyncJob {
ConfigurationSyncJob.run(
Job(variant: .configurationSync),
queue: .global(qos: .userInitiated),
success: { _, _ in resolver(Result.success(())) },
failure: { _, error, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _ in }
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, error, _, _ in resolver(Result.failure(error ?? HTTPError.generic)) },
deferred: { _, _ in },
using: dependencies
)
}
}

View File

@ -13,9 +13,10 @@ public enum DisappearingMessagesJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
// The 'backgroundTask' gets captured and cleared within the 'completion' block
let timestampNowMs: TimeInterval = TimeInterval(SnodeAPI.currentOffsetTimestampMs())
@ -37,7 +38,7 @@ public enum DisappearingMessagesJob: JobExecutor {
}
SNLog("[DisappearingMessagesJob] Deleted \(numDeleted) expired messages")
success(updatedJob ?? job, false)
success(updatedJob ?? job, false, dependencies)
// The 'if' is only there to prevent the "variable never read" warning from showing
if backgroundTask != nil { backgroundTask = nil }

View File

@ -13,20 +13,21 @@ public enum FailedAttachmentDownloadsJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
var changeCount: Int = -1
// Update all 'sending' message states to 'failed'
Storage.shared.write { db in
dependencies.storage.write { db in
changeCount = try Attachment
.filter(Attachment.Columns.state == Attachment.State.downloading)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedDownload))
}
SNLog("[FailedAttachmentDownloadsJob] Marked \(changeCount) attachments as failed")
success(job, false)
success(job, false, dependencies)
}
}

View File

@ -12,15 +12,16 @@ public enum FailedMessageSendsJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
var changeCount: Int = -1
var attachmentChangeCount: Int = -1
// Update all 'sending' message states to 'failed'
Storage.shared.write { db in
dependencies.storage.write { db in
let sendChangeCount: Int = try RecipientState
.filter(RecipientState.Columns.state == RecipientState.State.sending)
.updateAll(db, RecipientState.Columns.state.set(to: RecipientState.State.failed))
@ -34,6 +35,6 @@ public enum FailedMessageSendsJob: JobExecutor {
}
SNLog("[FailedMessageSendsJob] Marked \(changeCount) message\(changeCount == 1 ? "" : "s") as failed (\(attachmentChangeCount) upload\(attachmentChangeCount == 1 ? "" : "s") cancelled)")
success(job, false)
success(job, false, dependencies)
}
}

View File

@ -20,9 +20,10 @@ public enum GarbageCollectionJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, 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)
///
@ -32,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)
@ -56,7 +57,7 @@ public enum GarbageCollectionJob: JobExecutor {
return typesToCollect.asSet()
}()
Storage.shared.writeAsync(
dependencies.storage.writeAsync(
updates: { db in
/// Remove any typing indicators
if finalTypesToCollect.contains(.threadTypingIndicators) {
@ -368,7 +369,7 @@ public enum GarbageCollectionJob: JobExecutor {
// If we couldn't get the file lists then fail (invalid state and don't want to delete all attachment/profile files)
guard let fileInfo: FileInfo = maybeFileInfo else {
failure(job, StorageError.generic, false)
failure(job, StorageError.generic, false, dependencies)
return
}
@ -443,17 +444,17 @@ public enum GarbageCollectionJob: JobExecutor {
// Report a single file deletion as a job failure (even if other content was successfully removed)
guard deletionErrors.isEmpty else {
failure(job, (deletionErrors.first ?? StorageError.generic), false)
failure(job, (deletionErrors.first ?? StorageError.generic), false, dependencies)
return
}
// 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)
success(job, false, dependencies)
}
}
)

View File

@ -13,12 +13,13 @@ public enum GroupLeavingJob: JobExecutor {
public static var requiresInteractionId: Bool = true
public static func run(
_ job: SessionUtilitiesKit.Job,
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ())
{
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies = Dependencies()
) {
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
@ -26,13 +27,12 @@ public enum GroupLeavingJob: JobExecutor {
let interactionId: Int64 = job.interactionId
else {
SNLog("[GroupLeavingJob] Failed due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
let destination: Message.Destination = .closedGroup(groupPublicKey: threadId)
Storage.shared
dependencies.storage
.writePublisher { db in
guard (try? SessionThread.exists(db, id: threadId)) == true else {
SNLog("[GroupLeavingJob] Failed due to non-existent group conversation")
@ -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(
@ -71,7 +72,7 @@ public enum GroupLeavingJob: JobExecutor {
]
// Handle the appropriate response
Storage.shared.writeAsync { db in
dependencies.storage.writeAsync { db in
// If it failed due to one of these errors then clear out any associated data (as somehow
// the 'SessionThread' exists but not the data required to send the 'MEMBER_LEFT' message
// which would leave the user in a state where they can't leave the group)
@ -127,7 +128,7 @@ public enum GroupLeavingJob: JobExecutor {
)
}
success(job, false)
success(job, false, dependencies)
}
)
}

View File

@ -12,24 +12,23 @@ public enum MessageReceiveJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies = Dependencies()
) {
guard
let threadId: String = job.threadId,
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// Ensure no config messages are sent through this job
guard !details.messages.contains(where: { $0.variant == .sharedConfigMessage }) else {
SNLog("[MessageReceiveJob] Config messages incorrectly sent to the 'messageReceive' job")
failure(job, MessageReceiverError.invalidSharedConfigMessageHandling, true)
return
return failure(job, MessageReceiverError.invalidSharedConfigMessageHandling, true, dependencies)
}
var updatedJob: Job = job
@ -52,7 +51,7 @@ public enum MessageReceiveJob: JobExecutor {
}
}
Storage.shared.write { db in
dependencies.storage.write { db in
for (messageInfo, protoContent) in messageData {
do {
try MessageReceiver.handle(
@ -111,13 +110,13 @@ public enum MessageReceiveJob: JobExecutor {
// Handle the result
switch lastError {
case let error as MessageReceiverError where !error.isRetryable:
failure(updatedJob, error, true)
failure(updatedJob, error, true, dependencies)
case .some(let error):
failure(updatedJob, error, false)
failure(updatedJob, error, false, dependencies)
case .none:
success(updatedJob, false)
success(updatedJob, false, dependencies)
}
}
}

View File

@ -15,17 +15,17 @@ public enum MessageSendJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
SNLog("[MessageSendJob] Failing due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// We need to include 'fileIds' when sending messages with attachments to Open Groups
@ -46,14 +46,13 @@ public enum MessageSendJob: JobExecutor {
let interactionId: Int64 = job.interactionId
else {
SNLog("[MessageSendJob] Failing due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// Retrieve the current attachment state
typealias AttachmentState = (error: Error?, pendingUploadAttachmentIds: [String], preparedFileIds: [String])
let attachmentState: AttachmentState = Storage.shared
let attachmentState: AttachmentState = dependencies.storage
.read { db in
// If the original interaction no longer exists then don't bother sending the message (ie. the
// message was deleted before it even got sent)
@ -91,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
}
@ -104,26 +107,26 @@ public enum MessageSendJob: JobExecutor {
/// If we got an error when trying to retrieve the attachment state then this job is actually invalid so it
/// should permanently fail
guard attachmentState.error == nil else {
return failure(job, (attachmentState.error ?? MessageSenderError.invalidMessage), true)
return failure(job, (attachmentState.error ?? MessageSenderError.invalidMessage), true, dependencies)
}
/// If we have any pending (or failed) attachment uploads then we should create jobs for them and insert them into the
/// queue before the current job and defer it (this will mean the current job will re-run after these inserted jobs complete)
guard attachmentState.pendingUploadAttachmentIds.isEmpty else {
Storage.shared.write { db in
dependencies.storage.write { db in
try attachmentState.pendingUploadAttachmentIds
.filter { attachmentId in
// Don't add a new job if there is one already in the queue
!JobRunner.hasPendingOrRunningJob(
with: .attachmentUpload,
details: AttachmentUploadJob.Details(
!dependencies.jobRunner.hasJob(
of: .attachmentUpload,
with: AttachmentUploadJob.Details(
messageSendJobId: jobId,
attachmentId: attachmentId
)
)
}
.compactMap { attachmentId -> (jobId: Int64, job: Job)? in
JobRunner
dependencies.jobRunner
.insert(
db,
job: Job(
@ -150,7 +153,7 @@ public enum MessageSendJob: JobExecutor {
}
SNLog("[MessageSendJob] Deferring due to pending attachment uploads")
return deferred(job)
return deferred(job, dependencies)
}
// Store the fileIds so they can be sent with the open group message content
@ -164,7 +167,7 @@ public enum MessageSendJob: JobExecutor {
///
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job
/// so we shouldn't get here until attachments have already been uploaded
Storage.shared
dependencies.storage
.writePublisher { db in
try MessageSender.preparedSendData(
db,
@ -172,30 +175,31 @@ 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 {
case .finished: success(job, false)
case .finished: success(job, false, dependencies)
case .failure(let error):
SNLog("[MessageSendJob] Couldn't send message due to error: \(error).")
switch error {
case let senderError as MessageSenderError where !senderError.isRetryable:
failure(job, error, true)
failure(job, error, true, dependencies)
case OnionRequestAPIError.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 429: // Rate limited
failure(job, error, true)
failure(job, error, true, dependencies)
case SnodeAPIError.clockOutOfSync:
SNLog("[MessageSendJob] \(originalSentTimestamp != nil ? "Permanently Failing" : "Failing") to send \(type(of: details.message)) due to clock out of sync issue.")
failure(job, error, (originalSentTimestamp != nil))
failure(job, error, (originalSentTimestamp != nil), dependencies)
default:
SNLog("[MessageSendJob] Failed to send \(type(of: details.message)).")
@ -203,15 +207,14 @@ public enum MessageSendJob: JobExecutor {
if details.message is VisibleMessage {
guard
let interactionId: Int64 = job.interactionId,
Storage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true
dependencies.storage.read({ db in try Interaction.exists(db, id: interactionId) }) == true
else {
// The message has been deleted so permanently fail the job
failure(job, error, true)
return
return failure(job, error, true, dependencies)
}
}
failure(job, error, false)
failure(job, error, false, dependencies)
}
}
}

View File

@ -14,17 +14,17 @@ public enum NotifyPushServerJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
SNLog("[NotifyPushServerJob] Failing due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
PushNotificationAPI
@ -38,8 +38,8 @@ public enum NotifyPushServerJob: JobExecutor {
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: success(job, false)
case .failure(let error): failure(job, error, false)
case .finished: success(job, false, dependencies)
case .failure(let error): failure(job, error, false, dependencies)
}
}
)

View File

@ -12,13 +12,14 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
// Don't run when inactive or not in main app
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
deferred(job) // Don't need to do anything if it's not the main app
deferred(job, dependencies) // Don't need to do anything if it's not the main app
return
}
@ -26,7 +27,7 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
// in the database so we need to create a dummy one to retrieve the default room data
let defaultGroupId: String = OpenGroup.idFor(roomToken: "", server: OpenGroupAPI.defaultServer)
Storage.shared.write { db in
dependencies.storage.write { db in
guard try OpenGroup.exists(db, id: defaultGroupId) == false else { return }
_ = try OpenGroup(
@ -49,11 +50,11 @@ public enum RetrieveDefaultOpenGroupRoomsJob: JobExecutor {
switch result {
case .finished:
SNLog("[RetrieveDefaultOpenGroupRoomsJob] Successfully retrieved default Community rooms")
success(job, false)
success(job, false, dependencies)
case .failure(let error):
SNLog("[RetrieveDefaultOpenGroupRoomsJob] Failed to get default Community rooms")
failure(job, error, false)
failure(job, error, false, dependencies)
}
}
)

View File

@ -14,28 +14,27 @@ public enum SendReadReceiptsJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
let detailsData: Data = job.details,
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
failure(job, JobRunnerError.missingRequiredDetails, true)
return
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
// If there are no timestampMs values then the job can just complete (next time
// something is marked as read we want to try and run immediately so don't scuedule
// another run in this case)
guard !details.timestampMsValues.isEmpty else {
success(job, true)
return
return success(job, true, dependencies)
}
Storage.shared
dependencies.storage
.writePublisher { db in
try MessageSender.preparedSendData(
db,
@ -48,19 +47,19 @@ 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(
receiveCompletion: { result in
switch result {
case .failure(let error): failure(job, error, false)
case .failure(let error): failure(job, error, false, dependencies)
case .finished:
// When we complete the 'SendReadReceiptsJob' we want to immediately schedule
// 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
@ -87,7 +86,7 @@ public enum SendReadReceiptsJob: JobExecutor {
.saved(db)
}
success(updatedJob ?? job, shouldFinishCurrentJob)
success(updatedJob ?? job, shouldFinishCurrentJob, dependencies)
}
}
)

View File

@ -12,37 +12,37 @@ public enum UpdateProfilePictureJob: JobExecutor {
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool) -> (),
failure: @escaping (Job, Error?, Bool) -> (),
deferred: @escaping (Job) -> ()
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
// Don't run when inactive or not in main app
guard (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
deferred(job) // Don't need to do anything if it's not the main app
return
return deferred(job, dependencies) // Don't need to do anything if it's not the main app
}
// 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
if let jobId: Int64 = job.id {
Storage.shared.write { db in
dependencies.storage.write { db in
try Job
.filter(id: jobId)
.updateAll(db, Job.Columns.nextRunTimestamp.set(to: 0))
}
}
SNLog("[UpdateProfilePictureJob] Deferred as not enough time has passed since the last update")
deferred(job)
return
return deferred(job, dependencies)
}
// Note: The user defaults flag is updated in ProfileManager
let profile: Profile = Profile.fetchOrCreateCurrentUser()
let profile: Profile = Profile.fetchOrCreateCurrentUser(using: dependencies)
let profilePictureData: Data? = profile.profilePictureFileName
.map { ProfileManager.loadProfileData(with: $0) }
@ -56,12 +56,12 @@ public enum UpdateProfilePictureJob: JobExecutor {
// another database write
queue.async {
SNLog("[UpdateProfilePictureJob] Profile successfully updated")
success(job, false)
success(job, false, dependencies)
}
},
failure: { error in
SNLog("[UpdateProfilePictureJob] Failed to update profile")
failure(job, error, false)
failure(job, error, false, dependencies)
}
)
}

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(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 = Date().timeIntervalSince(lastOpen)
return 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
@ -1005,19 +984,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,
@ -1025,9 +1001,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
@ -1092,7 +1068,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
}
}
@ -1101,7 +1077,7 @@ public final class OpenGroupManager {
.shareReplay(1)
.eraseToAnyPublisher()
dependencies.mutableCache.mutate { cache in
dependencies.caches.mutate(cache: .openGroupManager) { cache in
cache.defaultRoomsPublisher = publisher
}
@ -1116,9 +1092,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
@ -1131,7 +1105,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 = 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 = (
@ -1145,14 +1119,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
@ -1226,10 +1200,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
}
@ -1237,9 +1211,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 }
@ -1253,90 +1283,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,109 +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 }
var NonceBytes: 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)
// Resubscribe for group push notifications
let currentUserPublicKey: String = 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 }
@ -167,7 +170,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 ?
@ -298,7 +302,7 @@ extension MessageReceiver {
.appending(quote?.attachmentId)
.appending(linkPreview?.attachmentId)
.forEach { attachmentId in
JobRunner.add(
dependencies.jobRunner.add(
db,
job: Job(
variant: .attachmentDownload,
@ -308,7 +312,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], Set<String>) 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
)
}
let allActiveLegacyGroupIds: Set<String> = try ClosedGroup
@ -129,7 +126,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(
// Resubscribe to all legacy groups
PushNotificationAPI.subscribeToLegacyGroups(
@ -144,7 +141,7 @@ extension MessageSender {
.handleEvents(
receiveOutput: { thread in
// Start polling
ClosedGroupPoller.shared.startIfNeeded(for: thread.id)
ClosedGroupPoller.shared.startIfNeeded(for: thread.id, using: dependencies)
}
)
.eraseToAnyPublisher()
@ -160,21 +157,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)
)
@ -202,7 +203,8 @@ extension MessageSender {
encryptedKeyPair: try MessageSender.encryptWithSessionProtocol(
db,
plaintext: plaintext,
for: memberPublicKey
for: memberPublicKey,
using: dependencies
)
)
}
@ -213,20 +215,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
@ -260,11 +263,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 {
@ -301,7 +305,8 @@ extension MessageSender {
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
interactionId: interactionId,
threadId: groupPublicKey,
threadVariant: .legacyGroup
threadVariant: .legacyGroup,
using: dependencies
)
// Update libSession
@ -330,7 +335,8 @@ extension MessageSender {
addedMembers: addedMembers,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
closedGroup: closedGroup,
using: dependencies
)
}
catch {
@ -357,7 +363,8 @@ extension MessageSender {
removedMembers: removedMembers,
userPublicKey: userPublicKey,
allGroupMembers: allGroupMembers,
closedGroup: closedGroup
closedGroup: closedGroup,
using: dependencies
)
.catch { _ in Fail(error: MessageSenderError.invalidClosedGroupUpdate).eraseToAnyPublisher() }
.eraseToAnyPublisher()
@ -373,7 +380,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
@ -428,7 +436,8 @@ extension MessageSender {
),
interactionId: interactionId,
threadId: closedGroup.threadId,
threadVariant: .legacyGroup
threadVariant: .legacyGroup,
using: dependencies
)
try addedMembers.forEach { member in
@ -455,7 +464,8 @@ extension MessageSender {
),
interactionId: nil,
threadId: member,
threadVariant: .contact
threadVariant: .contact,
using: dependencies
)
// Add the users to the group
@ -478,7 +488,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.")
@ -499,7 +510,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
@ -544,16 +555,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()
@ -570,9 +583,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(
@ -583,7 +597,7 @@ extension MessageSender {
timestampMs: SnodeAPI.currentOffsetTimestampMs()
).inserted(db)
JobRunner.upsert(
dependencies.jobRunner.upsert(
db,
job: Job(
variant: .groupLeaving,
@ -592,14 +606,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.")
@ -635,7 +652,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).")
@ -654,7 +672,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
@ -188,7 +188,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)
@ -203,7 +203,7 @@ public enum MessageReceiver {
message: message,
threadId: threadId,
threadVariant: threadVariant,
dependencies: dependencies
using: dependencies
)
switch message {
@ -227,7 +227,8 @@ public enum MessageReceiver {
db,
threadId: threadId,
threadVariant: threadVariant,
message: message
message: message,
using: dependencies
)
case let message as DataExtractionNotification:
@ -247,7 +248,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(
@ -269,7 +274,7 @@ public enum MessageReceiver {
try MessageReceiver.handleMessageRequestResponse(
db,
message: message,
dependencies: dependencies
using: dependencies
)
case let message as VisibleMessage:
@ -365,7 +370,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
@ -374,7 +379,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
@ -718,15 +718,16 @@ public final class MessageSender {
NotifyPushServerJob.run(
job,
queue: .global(qos: .default),
success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ in
// Always fulfill because the notify PN server job isn't critical.
resolver(Result.success(()))
},
deferred: { _ in
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

@ -25,11 +25,11 @@ public enum PushNotificationAPI {
public static func subscribe(
token: Data,
isForcedUpdate: Bool,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
let hexEncodedToken: String = token.toHexString()
let oldToken: String? = UserDefaults.standard[.deviceToken]
let lastUploadTime: Double = UserDefaults.standard[.lastDeviceTokenUpload]
let oldToken: String? = dependencies.standardUserDefaults[.deviceToken]
let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload]
let now: TimeInterval = Date().timeIntervalSince1970
guard isForcedUpdate || hexEncodedToken != oldToken || now - lastUploadTime > tokenExpirationInterval else {
@ -39,7 +39,7 @@ public enum PushNotificationAPI {
.eraseToAnyPublisher()
}
guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey() else {
guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies) else {
SNLog("Unable to retrieve PN encryption key.")
return Just(())
.setFailureType(to: Error.self)
@ -47,13 +47,13 @@ public enum PushNotificationAPI {
}
// TODO: Need to generate requests for each updated group as well
return Storage.shared
.readPublisher { db -> (SubscribeRequest, String, Set<String>) in
return dependencies.storage
.readPublisher(using: dependencies) { db -> (SubscribeRequest, String, Set<String>) in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let request: SubscribeRequest = SubscribeRequest(
pubkey: currentUserPublicKey,
namespaces: [.default],
@ -94,19 +94,20 @@ public enum PushNotificationAPI {
request: PushNotificationAPIRequest(
endpoint: .subscribe,
body: request
)
),
using: dependencies
)
.decoded(as: SubscribeResponse.self, using: dependencies)
.retry(maxRetryCount)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't subscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
UserDefaults.standard[.deviceToken] = hexEncodedToken
UserDefaults.standard[.lastDeviceTokenUpload] = now
UserDefaults.standard[.isUsingFullAPNs] = true
dependencies.standardUserDefaults[.deviceToken] = hexEncodedToken
dependencies.standardUserDefaults[.lastDeviceTokenUpload] = now
dependencies.standardUserDefaults[.isUsingFullAPNs] = true
},
receiveCompletion: { result in
switch result {
@ -122,7 +123,8 @@ public enum PushNotificationAPI {
forced: true,
token: hexEncodedToken,
currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: legacyGroupIds
legacyGroupIds: legacyGroupIds,
using: dependencies
)
]
)
@ -135,16 +137,16 @@ public enum PushNotificationAPI {
public static func unsubscribe(
token: Data,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
let hexEncodedToken: String = token.toHexString()
// FIXME: Remove this once legacy groups are deprecated
/// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case)
Storage.shared
.readPublisher { db -> (String, Set<String>) in
dependencies.storage
.readPublisher(using: dependencies) { db -> (String, Set<String>) in
(
getUserHexEncodedPublicKey(db),
getUserHexEncodedPublicKey(db, using: dependencies),
try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
@ -168,18 +170,18 @@ public enum PushNotificationAPI {
.collect()
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete()
// TODO: Need to generate requests for each updated group as well
return Storage.shared
.readPublisher { db -> UnsubscribeRequest in
return dependencies.storage
.readPublisher(using: dependencies) { db -> UnsubscribeRequest in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair
}
return UnsubscribeRequest(
pubkey: getUserHexEncodedPublicKey(db),
pubkey: getUserHexEncodedPublicKey(db, using: dependencies),
serviceInfo: UnsubscribeRequest.ServiceInfo(
token: hexEncodedToken
),
@ -195,17 +197,18 @@ public enum PushNotificationAPI {
request: PushNotificationAPIRequest(
endpoint: .unsubscribe,
body: request
)
),
using: dependencies
)
.decoded(as: UnsubscribeResponse.self, using: dependencies)
.retry(maxRetryCount)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't unsubscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
UserDefaults.standard[.deviceToken] = nil
dependencies.standardUserDefaults[.deviceToken] = nil
},
receiveCompletion: { result in
switch result {
@ -227,7 +230,7 @@ public enum PushNotificationAPI {
recipient: String,
with message: String,
maxRetryCount: Int? = nil,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return PushNotificationAPI
.send(
@ -237,10 +240,11 @@ public enum PushNotificationAPI {
data: message,
sendTo: recipient
)
)
),
using: dependencies
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount)
.retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -266,14 +270,14 @@ public enum PushNotificationAPI {
token: String? = nil,
currentUserPublicKey: String,
legacyGroupIds: Set<String>,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs]
let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs]
// Only continue if PNs are enabled and we have a device token
guard
(forced || isUsingFullAPNs),
let deviceToken: String = (token ?? UserDefaults.standard[.deviceToken])
let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken])
else {
return Just(())
.setFailureType(to: Error.self)
@ -290,10 +294,11 @@ public enum PushNotificationAPI {
device: "ios",
legacyGroupPublicKeys: legacyGroupIds
)
)
),
using: dependencies
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -315,7 +320,7 @@ public enum PushNotificationAPI {
public static func unsubscribeFromLegacyGroup(
legacyGroupId: String,
currentUserPublicKey: String,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return PushNotificationAPI
.send(
@ -325,10 +330,11 @@ public enum PushNotificationAPI {
pubKey: currentUserPublicKey,
closedGroupPublicKey: legacyGroupId
)
)
),
using: dependencies
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -350,7 +356,7 @@ public enum PushNotificationAPI {
public static func processNotification(
notificationContent: UNNotificationContent,
dependencies: SMKDependencies = SMKDependencies()
dependencies: Dependencies = Dependencies()
) -> (envelope: SNProtoEnvelope?, result: ProcessResult) {
// Make sure the notification is from the updated push server
guard notificationContent.userInfo["spns"] != nil else {
@ -369,18 +375,20 @@ public enum PushNotificationAPI {
guard
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
let encData: Data = Data(base64Encoded: base64EncodedEncString),
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(),
encData.count > dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes)
else { return (nil, .failure) }
let nonce: Data = encData[0..<dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes]
let payload: Data = encData[dependencies.aeadXChaCha20Poly1305Ietf.NonceBytes...]
let nonce: Data = encData[0..<dependencies.crypto.size(.aeadXChaCha20NonceBytes)]
let payload: Data = encData[dependencies.crypto.size(.aeadXChaCha20NonceBytes)...]
guard
let paddedData: [UInt8] = dependencies.aeadXChaCha20Poly1305Ietf.decrypt(
authenticatedCipherText: payload.bytes,
secretKey: notificationsEncryptionKey.bytes,
nonce: nonce.bytes
let paddedData: [UInt8] = try? dependencies.crypto.perform(
.decryptAeadXChaCha20(
authenticatedCipherText: payload.bytes,
secretKey: notificationsEncryptionKey.bytes,
nonce: nonce.bytes
)
)
else { return (nil, .failure) }
@ -408,7 +416,7 @@ public enum PushNotificationAPI {
// MARK: - Security
@discardableResult private static func getOrGenerateEncryptionKey() throws -> Data {
@discardableResult private static func getOrGenerateEncryptionKey(using dependencies: Dependencies) throws -> Data {
do {
var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data(
forService: keychainService,
@ -461,7 +469,7 @@ public enum PushNotificationAPI {
private static func send<T: Encodable>(
request: PushNotificationAPIRequest<T>,
using dependencies: SSKDependencies = SSKDependencies()
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
guard
let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.rawValue)"),
@ -487,8 +495,14 @@ public enum PushNotificationAPI {
urlRequest.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
urlRequest.httpBody = payload
return dependencies.onionApi
.sendOnionRequest(urlRequest, to: request.endpoint.server, with: request.endpoint.serverPublicKey)
return dependencies.network
.send(
.onionRequest(
urlRequest,
to: request.endpoint.server,
with: request.endpoint.serverPublicKey
)
)
.eraseToAnyPublisher()
}
}

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)
@ -77,12 +77,8 @@ 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 {
@ -333,8 +330,13 @@ public class Poller {
// If we are force-polling then add to the JobRunner so they are
// persistent and will retry on the next app run if they fail but
// don't let them auto-start
let updatedJob: Job? = JobRunner
.add(db, job: jobToRun, canStartJob: !calledFromBackgroundPoller)
let updatedJob: Job? = dependencies.jobRunner
.add(
db,
job: jobToRun,
canStartJob: !calledFromBackgroundPoller,
using: dependencies
)
return updatedJob?.id
}
@ -362,8 +364,13 @@ public class Poller {
// If we are force-polling then add to the JobRunner so they are
// persistent and will retry on the next app run if they fail but
// don't let them auto-start
let updatedJob: Job? = JobRunner
.add(db, job: jobToRun, canStartJob: !calledFromBackgroundPoller)
let updatedJob: Job? = dependencies.jobRunner
.add(
db,
job: jobToRun,
canStartJob: !calledFromBackgroundPoller,
using: dependencies
)
// Create the dependency between the jobs
if let updatedJobId: Int64 = updatedJob?.id {
@ -419,10 +426,11 @@ public class Poller {
// Note: In the background we just want jobs to fail silently
ConfigMessageReceiveJob.run(
job,
queue: dependencies.receiveQueue,
success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) }
queue: Threading.pollerQueue,
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ in resolver(Result.success(())) },
deferred: { _, _ in resolver(Result.success(())) },
using: dependencies
)
}
}
@ -439,10 +447,11 @@ public class Poller {
// Note: In the background we just want jobs to fail silently
MessageReceiveJob.run(
job,
queue: dependencies.receiveQueue,
success: { _, _ in resolver(Result.success(())) },
failure: { _, _, _ in resolver(Result.success(())) },
deferred: { _ in resolver(Result.success(())) }
queue: Threading.pollerQueue,
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, _, _, _ 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

@ -4,6 +4,9 @@
#import "AppReadiness.h"
#import "AppContext.h"
#import <SignalCoreKit/SignalCoreKit.h>
#import <SignalCoreKit/Threading.h>
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
NS_ASSUME_NONNULL_BEGIN
@ -63,7 +66,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)runNowOrWhenAppWillBecomeReady:(AppReadyBlock)block
{
if (CurrentAppContext().isRunningTests) {
if ([SNUtilitiesKitConfiguration isRunningTests]) {
// We don't need to do any "on app ready" work in the tests.
return;
}
@ -90,7 +93,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)runNowOrWhenAppDidBecomeReady:(AppReadyBlock)block
{
if (CurrentAppContext().isRunningTests) {
if ([SNUtilitiesKitConfiguration isRunningTests]) {
// We don't need to do any "on app ready" work in the tests.
return;
}

View File

@ -0,0 +1,152 @@
// 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: - AeadXChaCha20Poly1305Ietf
public extension Crypto.Size {
static let aeadXChaCha20NonceBytes: Crypto.Size = Crypto.Size(id: "aeadXChaCha20NonceBytes") {
Sodium().aead.xchacha20poly1305ietf.NonceBytes
}
}
// 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

@ -0,0 +1,419 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Quick
import Nimble
@testable import SessionMessagingKit
@testable import SessionUtilitiesKit
class MessageSendJobSpec: QuickSpec {
// MARK: - Spec
override func spec() {
var job: Job!
var interaction: Interaction!
var attachment: Attachment!
var interactionAttachment: InteractionAttachment!
var mockStorage: Storage!
var mockJobRunner: MockJobRunner!
var dependencies: Dependencies!
// MARK: - JobRunner
describe("a MessageSendJob") {
// MARK: - Configuration
beforeEach {
mockStorage = SynchronousStorage(
customWriter: try! DatabaseQueue(),
customMigrationTargets: [
SNUtilitiesKit.self,
SNMessagingKit.self
]
)
mockJobRunner = MockJobRunner()
dependencies = Dependencies(
storage: mockStorage,
jobRunner: mockJobRunner,
dateNow: Date(timeIntervalSince1970: 1234567890)
)
attachment = Attachment(
id: "200",
variant: .standard,
state: .failedDownload,
contentType: "text/plain",
byteCount: 200
)
mockStorage.write { db in
try SessionThread.fetchOrCreate(db, id: "Test1", variant: .contact, shouldBeVisible: true)
}
mockJobRunner
.when {
$0.jobInfoFor(
jobs: nil,
state: .running,
variant: .attachmentUpload
)
}
.thenReturn([:])
mockJobRunner
.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
job.id = 1000
try! job.insert(db)
}
.thenReturn((1000, Job(variant: .messageSend)))
}
afterEach {
job = nil
mockStorage = nil
dependencies = nil
}
// MARK: - fails when not given any details
it("fails when not given any details") {
job = Job(variant: .messageSend)
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 },
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
// MARK: - fails when given incorrect details
it("fails when given incorrect details") {
job = Job(
variant: .messageSend,
details: MessageReceiveJob.Details(messages: [], calledFromBackgroundPoller: false)
)
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 },
using: dependencies
)
expect(error).to(matchError(JobRunnerError.missingRequiredDetails))
expect(permanentFailure).to(beTrue())
}
// MARK: - of VisibleMessage
context("of VisibleMessage") {
beforeEach {
interaction = Interaction(
id: 100,
serverHash: nil,
messageUuid: nil,
threadId: "Test1",
authorId: "Test",
variant: .standardOutgoing,
body: "Test",
timestampMs: 1234567890,
receivedAtTimestampMs: 1234567900,
wasRead: false,
hasMention: false,
expiresInSeconds: nil,
expiresStartedAtMs: nil,
linkPreviewUrl: nil,
openGroupServerMessageId: nil,
openGroupWhisperMods: false,
openGroupWhisperTo: nil
)
job = Job(
variant: .messageSend,
interactionId: interaction.id!,
details: MessageSendJob.Details(
destination: .contact(publicKey: "Test"),
message: VisibleMessage(
text: "Test"
)
)
)
mockStorage.write { db in
try interaction.insert(db)
try job.insert(db)
}
}
// MARK: -- fails when there is no job id
it("fails when there is no job id") {
job = Job(
variant: .messageSend,
interactionId: interaction.id!,
details: MessageSendJob.Details(
destination: .contact(publicKey: "Test"),
message: VisibleMessage(
text: "Test"
)
)
)
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 },
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,
details: MessageSendJob.Details(
destination: .contact(publicKey: "Test"),
message: VisibleMessage(
text: "Test"
)
)
)
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 },
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,
interactionId: 12345,
details: MessageSendJob.Details(
destination: .contact(publicKey: "Test"),
message: VisibleMessage(
text: "Test"
)
)
)
mockStorage.write { db in try job.insert(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 },
using: dependencies
)
expect(error).to(matchError(StorageError.objectNotFound))
expect(permanentFailure).to(beTrue())
}
// MARK: -- with an attachment
context("with an attachment") {
beforeEach {
interactionAttachment = InteractionAttachment(
albumIndex: 0,
interactionId: interaction.id!,
attachmentId: attachment.id
)
mockStorage.write { db in
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 attachment.with(state: .failedDownload).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 },
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 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 attachment.with(state: .uploading).save(db)
}
MessageSendJob.run(
job,
queue: .main,
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in didDefer = true },
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 {
$0.jobInfoFor(
jobs: nil,
state: .running,
variant: .attachmentUpload
)
}
.thenReturn([:])
MessageSendJob.run(
job,
queue: .main,
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in },
using: dependencies
)
expect(mockJobRunner)
.to(call(.exactly(times: 1), matchingParameters: true) {
$0.insert(
any(),
job: Job(
variant: .attachmentUpload,
behaviour: .runOnce,
shouldBlock: false,
shouldSkipLaunchBecomeActive: false,
interactionId: 100,
details: AttachmentUploadJob.Details(
messageSendJobId: 1,
attachmentId: "200"
)
),
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,
queue: .main,
success: { _, _, _ in },
failure: { _, _, _, _ in },
deferred: { _, _ in },
using: dependencies
)
expect(mockStorage.read { db in try JobDependencies.fetchOne(db) })
.to(equal(JobDependencies(jobId: 9, dependantId: 1000)))
}
}
}
}
}
}
}

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,20 +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
var NonceBytes: Int = 24
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
}
}

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