Re-organize files
This commit is contained in:
parent
41d078d012
commit
a3382f41d4
2
Podfile
2
Podfile
|
@ -121,7 +121,7 @@ target 'SessionSnodeKit' do
|
|||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
end
|
||||
|
||||
target 'SessionUtilities' do
|
||||
target 'SessionUtilitiesKit' do
|
||||
pod 'CryptoSwift', :inhibit_warnings => true
|
||||
pod 'Curve25519Kit', :inhibit_warnings => true
|
||||
pod 'PromiseKit', :inhibit_warnings => true
|
||||
|
|
|
@ -332,6 +332,6 @@ SPEC CHECKSUMS:
|
|||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||
|
||||
PODFILE CHECKSUM: 2efffb9efc5ed9feaf8b2df90d8dee2d4685e7c2
|
||||
PODFILE CHECKSUM: 278b25019daa575575de0bf9baf371f7cdcd4fc4
|
||||
|
||||
COCOAPODS: 1.10.0.rc.1
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
// TODO: Implementation
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
// TODO: Implementation
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class JobQueue : JobDelegate {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class MessageReceiveJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public var delegate: JobDelegate?
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public var delegate: JobDelegate?
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class NotifyPNServerJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public var delegate: JobDelegate?
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SessionProtocolKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNClosedGroupUpdate)
|
||||
public final class ClosedGroupUpdate : ControlMessage {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNExpirationTimerUpdate)
|
||||
public final class ExpirationTimerUpdate : ControlMessage {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNReadReceipt)
|
||||
public final class ReadReceipt : ControlMessage {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SessionProtocolKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNSessionRequest)
|
||||
public final class SessionRequest : ControlMessage {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNTypingIndicator)
|
||||
public final class TypingIndicator : ControlMessage {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension VisibleMessage {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension VisibleMessage {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension VisibleMessage {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNVisibleMessage)
|
||||
public final class VisibleMessage : Message {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CryptoSwift
|
||||
import SessionProtocolKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension MessageReceiver {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal enum MessageReceiver {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SessionProtocolKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension MessageSender {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal enum MessageSender {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SessionSnodeKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public enum MessageWrapper {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import SessionSnodeKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
enum ProofOfWork {
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class ClosedGroupRatchet : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public let chainKey: String
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public protocol SharedSenderKeysDelegate {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CryptoSwift
|
||||
import Curve25519Kit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
/// A fallback session cipher which uses the the recipient's public key to encrypt data.
|
||||
@objc public final class FallBackSessionCipher : NSObject {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
internal extension OnionRequestAPI {
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||
public enum OnionRequestAPI {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PromiseKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public enum SnodeAPI {
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import PromiseKit
|
||||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public final class SnodeMessage : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
/// The hex encoded public key of the recipient.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import SessionUtilities
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public protocol SessionSnodeKitStorageProtocol {
|
||||
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
FOUNDATION_EXPORT double SessionUtilitiesVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char SessionUtilitiesVersionString[];
|
||||
|
||||
#import <SessionUtilities/ECKeyPair+Utilities.h>
|
||||
#import <SessionUtilities/NSDate+Timestamp.h>
|
||||
#import <SessionUtilities/NSTimer+Proxying.h>
|
||||
#import <SessionUtilities/TSRequest.h>
|
|
@ -0,0 +1,9 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
FOUNDATION_EXPORT double SessionUtilitiesKitVersionNumber;
|
||||
FOUNDATION_EXPORT const unsigned char SessionUtilitiesKitVersionString[];
|
||||
|
||||
#import <SessionUtilitiesKit/ECKeyPair+Utilities.h>
|
||||
#import <SessionUtilitiesKit/NSDate+Timestamp.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
#import <SessionUtilitiesKit/TSRequest.h>
|
|
@ -24,8 +24,7 @@ public enum Mnemonic {
|
|||
if let cachedResult = Language.wordSetCache[self] {
|
||||
return cachedResult
|
||||
} else {
|
||||
let bundleID = "org.cocoapods.SessionServiceKit"
|
||||
let url = Bundle(identifier: bundleID)!.url(forResource: filename, withExtension: "txt")!
|
||||
let url = Bundle.main.url(forResource: filename, withExtension: "txt")!
|
||||
let contents = try! String(contentsOf: url)
|
||||
let result = contents.split(separator: ",").map { String($0) }
|
||||
Language.wordSetCache[self] = result
|
|
@ -470,7 +470,7 @@
|
|||
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
||||
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
|
||||
9C9B845C8451114076E55902 /* Pods_SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 686875887229AB29C07145BA /* Pods_SessionUtilities.framework */; };
|
||||
945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */; };
|
||||
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; };
|
||||
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
|
||||
|
@ -652,7 +652,8 @@
|
|||
C3A71D682558A0170043A11F /* LokiSessionCipher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D652558A0170043A11F /* LokiSessionCipher.swift */; };
|
||||
C3A71D742558A0F60043A11F /* SMKProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D722558A0F60043A11F /* SMKProto.swift */; };
|
||||
C3A71D752558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */; };
|
||||
C3A71D862558A28A0043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
|
||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
|
||||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
|
||||
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE0752554CDA60050F1E3 /* Configuration.swift */; };
|
||||
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3BBE07F2554CDD70050F1E3 /* Storage.swift */; };
|
||||
|
@ -679,10 +680,10 @@
|
|||
C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; };
|
||||
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; };
|
||||
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; };
|
||||
C3C2A67D255388CC00C340D1 /* SessionUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
|
||||
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
|
||||
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||
C3C2A6F425539DE700C340D1 /* SessionMessagingKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3C2A6F725539DE700C340D1 /* SessionMessagingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; };
|
||||
C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -794,7 +795,7 @@
|
|||
C3C2ABF82553C8A300C340D1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2ABF72553C8A300C340D1 /* Storage.swift */; };
|
||||
C3C2AC0A2553C9A100C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC092553C9A100C340D1 /* Configuration.swift */; };
|
||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */; };
|
||||
C3C2AC372553CCE600C340D1 /* SessionUtilities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */; };
|
||||
C3C2AC372553CCE600C340D1 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
|
||||
C3C3CF8924D8EED300E1CCE7 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
|
||||
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
|
||||
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
||||
|
@ -926,7 +927,7 @@
|
|||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
C3C2A86A2553B41A00C340D1 /* SessionProtocolKit.framework in Embed Frameworks */,
|
||||
C3C2A681255388CC00C340D1 /* SessionUtilities.framework in Embed Frameworks */,
|
||||
C3C2A681255388CC00C340D1 /* SessionUtilitiesKit.framework in Embed Frameworks */,
|
||||
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
|
||||
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
|
||||
C3C2A6F825539DE700C340D1 /* SessionMessagingKit.framework in Embed Frameworks */,
|
||||
|
@ -945,6 +946,7 @@
|
|||
1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = "<group>"; };
|
||||
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionProtocolKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2400888D239F30A600305217 /* SessionRestorationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRestorationView.swift; sourceTree = "<group>"; };
|
||||
264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3303495F6651CE2F3CC9693B /* Pods-SessionUtilities.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilities.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilities/Pods-SessionUtilities.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
3403B95B20EA9526001A1F44 /* OWSContactShareButtonsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactShareButtonsView.m; sourceTree = "<group>"; };
|
||||
|
@ -1476,7 +1478,6 @@
|
|||
4CFD151C22415AA400F2450F /* CallVideoHintView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallVideoHintView.swift; sourceTree = "<group>"; };
|
||||
4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LegacyNotificationsAdaptee.swift; path = UserInterface/Notifications/LegacyNotificationsAdaptee.swift; sourceTree = "<group>"; };
|
||||
4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = "<group>"; };
|
||||
686875887229AB29C07145BA /* Pods_SessionUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
6A26D6558DE69AF455E571C1 /* Pods-SessionMessagingKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; };
|
||||
|
@ -1491,8 +1492,10 @@
|
|||
7BDCFC0424206E7300641C39 /* LokiPushNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = LokiPushNotificationService.entitlements; sourceTree = "<group>"; };
|
||||
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationServiceExtensionContext.swift; sourceTree = "<group>"; };
|
||||
7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderIcon.swift; sourceTree = "<group>"; };
|
||||
7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionUtilitiesKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionUtilitiesKit/Pods-SessionUtilitiesKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
8981C8F64D94D3C52EB67A2C /* Pods-SignalTests.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.test.xcconfig"; sourceTree = "<group>"; };
|
||||
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||
9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
948239851C08032C842937CC /* Pods-SignalMessaging.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.test.xcconfig"; sourceTree = "<group>"; };
|
||||
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
9B533A9FA46206D3D99C9ADA /* Pods-SignalMessaging.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalMessaging.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalMessaging/Pods-SignalMessaging.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -1689,6 +1692,7 @@
|
|||
C3A71D662558A0170043A11F /* DiffieHellman.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffieHellman.swift; sourceTree = "<group>"; };
|
||||
C3A71D722558A0F60043A11F /* SMKProto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SMKProto.swift; path = Protos/SMKProto.swift; sourceTree = "<group>"; };
|
||||
C3A71D732558A0F60043A11F /* OWSUnidentifiedDelivery.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSUnidentifiedDelivery.pb.swift; path = Protos/OWSUnidentifiedDelivery.pb.swift; sourceTree = "<group>"; };
|
||||
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
|
||||
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = "<group>"; };
|
||||
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
C3BBE0752554CDA60050F1E3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
|
@ -1719,8 +1723,8 @@
|
|||
C3C2A5D72553860B00C340D1 /* AESGCM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AESGCM.swift; sourceTree = "<group>"; };
|
||||
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
|
||||
C3C2A5D92553860B00C340D1 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
|
||||
C3C2A679255388CC00C340D1 /* SessionUtilities.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilities.h; sourceTree = "<group>"; };
|
||||
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionUtilitiesKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionUtilitiesKit.h; sourceTree = "<group>"; };
|
||||
C3C2A67C255388CC00C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionMessagingKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C3C2A6F225539DE700C340D1 /* SessionMessagingKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionMessagingKit.h; sourceTree = "<group>"; };
|
||||
|
@ -1901,7 +1905,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
|
||||
C3C2A6C62553896A00C340D1 /* SessionUtilities.framework in Frameworks */,
|
||||
C3C2A6C62553896A00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1909,7 +1913,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9C9B845C8451114076E55902 /* Pods_SessionUtilities.framework in Frameworks */,
|
||||
945AA2B82B621254F69FA9E8 /* Pods_SessionUtilitiesKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1928,7 +1932,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A33A4BA9D050805FE156E3ED /* Pods_SessionProtocolKit.framework in Frameworks */,
|
||||
C3C2AC372553CCE600C340D1 /* SessionUtilities.framework in Frameworks */,
|
||||
C3C2AC372553CCE600C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -1959,7 +1963,7 @@
|
|||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */,
|
||||
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */,
|
||||
A1C32D5117A06544000A904E /* AddressBook.framework in Frameworks */,
|
||||
C3C2A680255388CC00C340D1 /* SessionUtilities.framework in Frameworks */,
|
||||
C3C2A680255388CC00C340D1 /* SessionUtilitiesKit.framework in Frameworks */,
|
||||
A1C32D5017A06538000A904E /* AddressBookUI.framework in Frameworks */,
|
||||
D2AEACDC16C426DA00C364C0 /* CFNetwork.framework in Frameworks */,
|
||||
D2179CFE16BB0B480006F3AB /* SystemConfiguration.framework in Frameworks */,
|
||||
|
@ -2979,6 +2983,8 @@
|
|||
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */,
|
||||
AEA8083C060FF9BAFF6E0C9F /* Pods-SessionProtocolKit.debug.xcconfig */,
|
||||
174BD0AE74771D02DAC2B7A9 /* Pods-SessionProtocolKit.app store release.xcconfig */,
|
||||
264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */,
|
||||
7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3421,7 +3427,7 @@
|
|||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3C2A67A255388CC00C340D1 /* SessionUtilities */ = {
|
||||
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3C2A68B255388D500C340D1 /* Meta */,
|
||||
|
@ -3435,6 +3441,7 @@
|
|||
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
|
||||
C3C2A5D92553860B00C340D1 /* JSON.swift */,
|
||||
C3C2A5CE2553860700C340D1 /* Logging.swift */,
|
||||
C3A71F882558BA9F0043A11F /* Mnemonic.swift */,
|
||||
C300A6302554B68200555489 /* NSDate+Timestamp.h */,
|
||||
C300A6312554B6D100555489 /* NSDate+Timestamp.mm */,
|
||||
C352A3762557859C00338F3E /* NSTimer+Proxying.h */,
|
||||
|
@ -3444,13 +3451,13 @@
|
|||
C352A3A42557B5F000338F3E /* TSRequest.h */,
|
||||
C352A3A52557B60D00338F3E /* TSRequest.m */,
|
||||
);
|
||||
path = SessionUtilities;
|
||||
path = SessionUtilitiesKit;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3C2A68B255388D500C340D1 /* Meta */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3C2A67B255388CC00C340D1 /* SessionUtilities.h */,
|
||||
C3C2A67B255388CC00C340D1 /* SessionUtilitiesKit.h */,
|
||||
C3C2A67C255388CC00C340D1 /* Info.plist */,
|
||||
);
|
||||
path = Meta;
|
||||
|
@ -3702,7 +3709,7 @@
|
|||
C3C2A6F125539DE700C340D1 /* SessionMessagingKit */,
|
||||
C3C2A8632553B41A00C340D1 /* SessionProtocolKit */,
|
||||
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
|
||||
C3C2A67A255388CC00C340D1 /* SessionUtilities */,
|
||||
C3C2A67A255388CC00C340D1 /* SessionUtilitiesKit */,
|
||||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||
D221A08A169C9E5E00537ABF /* Products */,
|
||||
9404664EC513585B05DF1350 /* Pods */,
|
||||
|
@ -3718,7 +3725,7 @@
|
|||
453518921FC63DBF00210559 /* SignalMessaging.framework */,
|
||||
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
|
||||
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
|
||||
C3C2A679255388CC00C340D1 /* SessionUtilities.framework */,
|
||||
C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */,
|
||||
C3C2A6F025539DE700C340D1 /* SessionMessagingKit.framework */,
|
||||
C3C2A8622553B41A00C340D1 /* SessionProtocolKit.framework */,
|
||||
);
|
||||
|
@ -3769,9 +3776,9 @@
|
|||
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
|
||||
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
|
||||
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
|
||||
686875887229AB29C07145BA /* Pods_SessionUtilities.framework */,
|
||||
FB523C549815DE935E98151E /* Pods_SessionMessagingKit.framework */,
|
||||
2183DCA28E0620BC73FCC554 /* Pods_SessionProtocolKit.framework */,
|
||||
9117261809D69B3D7C26B8F1 /* Pods_SessionUtilitiesKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3905,7 +3912,7 @@
|
|||
C300A63B2554B72200555489 /* NSDate+Timestamp.h in Headers */,
|
||||
C352A3772557864000338F3E /* NSTimer+Proxying.h in Headers */,
|
||||
C3471F5625553E1100297E91 /* ECKeyPair+Utilities.h in Headers */,
|
||||
C3C2A67D255388CC00C340D1 /* SessionUtilities.h in Headers */,
|
||||
C3C2A67D255388CC00C340D1 /* SessionUtilitiesKit.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -4051,9 +4058,9 @@
|
|||
productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
C3C2A678255388CC00C340D1 /* SessionUtilities */ = {
|
||||
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */;
|
||||
buildConfigurationList = C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */;
|
||||
buildPhases = (
|
||||
83DABC75697364620557C68B /* [CP] Check Pods Manifest.lock */,
|
||||
C3C2A674255388CC00C340D1 /* Headers */,
|
||||
|
@ -4065,9 +4072,9 @@
|
|||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = SessionUtilities;
|
||||
name = SessionUtilitiesKit;
|
||||
productName = SessionUtilities;
|
||||
productReference = C3C2A679255388CC00C340D1 /* SessionUtilities.framework */;
|
||||
productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */ = {
|
||||
|
@ -4299,7 +4306,7 @@
|
|||
C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */,
|
||||
C3C2A8612553B41A00C340D1 /* SessionProtocolKit */,
|
||||
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
|
||||
C3C2A678255388CC00C340D1 /* SessionUtilities */,
|
||||
C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -4708,7 +4715,7 @@
|
|||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-SessionUtilities-checkManifestLockResult.txt",
|
||||
"$(DERIVED_FILE_DIR)/Pods-SessionUtilitiesKit-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
|
@ -5017,15 +5024,16 @@
|
|||
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */,
|
||||
C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */,
|
||||
C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */,
|
||||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
|
||||
C3471F6825553E7600297E91 /* ECKeyPair+Utilities.m in Sources */,
|
||||
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */,
|
||||
C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */,
|
||||
C352A3A62557B60D00338F3E /* TSRequest.m in Sources */,
|
||||
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */,
|
||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
|
||||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */,
|
||||
C300A60D2554B31900555489 /* Logging.swift in Sources */,
|
||||
C3A71D862558A28A0043A11F /* DiffieHellman.swift in Sources */,
|
||||
C300A6322554B6D100555489 /* NSDate+Timestamp.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -5477,7 +5485,7 @@
|
|||
};
|
||||
C3C2A67F255388CC00C340D1 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = C3C2A678255388CC00C340D1 /* SessionUtilities */;
|
||||
target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */;
|
||||
targetProxy = C3C2A67E255388CC00C340D1 /* PBXContainerItemProxy */;
|
||||
};
|
||||
C3C2A6F625539DE700C340D1 /* PBXTargetDependency */ = {
|
||||
|
@ -6015,7 +6023,7 @@
|
|||
};
|
||||
C3C2A682255388CC00C340D1 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = E7E2FBF1546840C91B7E4879 /* Pods-SessionUtilities.debug.xcconfig */;
|
||||
baseConfigurationReference = 264033E641846B67E0CB21B0 /* Pods-SessionUtilitiesKit.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
|
@ -6047,7 +6055,7 @@
|
|||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
|
||||
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
|
@ -6062,7 +6070,7 @@
|
|||
"-framework",
|
||||
"\"UIKit\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
|
@ -6076,7 +6084,7 @@
|
|||
};
|
||||
C3C2A683255388CC00C340D1 /* App Store Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 3303495F6651CE2F3CC9693B /* Pods-SessionUtilities.app store release.xcconfig */;
|
||||
baseConfigurationReference = 7DD180F770F8518B4E8796F2 /* Pods-SessionUtilitiesKit.app store release.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
|
@ -6129,7 +6137,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
INFOPLIST_FILE = SessionUtilities/Meta/Info.plist;
|
||||
INFOPLIST_FILE = SessionUtilitiesKit/Meta/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
|
@ -6144,7 +6152,7 @@
|
|||
"-framework",
|
||||
"\"UIKit\"",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilities";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionUtilitiesKit";
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -6854,7 +6862,7 @@
|
|||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = "App Store Release";
|
||||
};
|
||||
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilities" */ = {
|
||||
C3C2A684255388CC00C340D1 /* Build configuration list for PBXNativeTarget "SessionUtilitiesKit" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C3C2A682255388CC00C340D1 /* Debug */,
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
import CryptoSwift
|
||||
|
||||
private extension UInt64 {
|
||||
|
||||
fileprivate init(_ decimal: Decimal) {
|
||||
self.init(truncating: decimal as NSDecimalNumber)
|
||||
}
|
||||
|
||||
// Convert a UInt8 array to a UInt64
|
||||
fileprivate init(_ bytes: [UInt8]) {
|
||||
precondition(bytes.count <= MemoryLayout<UInt64>.size)
|
||||
var value: UInt64 = 0
|
||||
for byte in bytes {
|
||||
value <<= 8
|
||||
value |= UInt64(byte)
|
||||
}
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MutableCollection where Element == UInt8, Index == Int {
|
||||
|
||||
/// Increment every element by the given amount
|
||||
///
|
||||
/// - Parameter amount: The amount to increment by
|
||||
/// - Returns: The incremented collection
|
||||
fileprivate func increment(by amount: Int) -> Self {
|
||||
var result = self
|
||||
var increment = amount
|
||||
for i in (0..<result.count).reversed() {
|
||||
guard increment > 0 else { break }
|
||||
let sum = Int(result[i]) + increment
|
||||
result[i] = UInt8(sum % 256)
|
||||
increment = sum / 256
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main proof of work logic.
|
||||
*
|
||||
* This was copied from the desktop messenger.
|
||||
* Ref: libloki/proof-of-work.js
|
||||
*/
|
||||
public enum ProofOfWork {
|
||||
|
||||
// If this changes then we also have to use something other than UInt64 to support the new length
|
||||
private static let nonceLength = 8
|
||||
|
||||
/// Calculate a proof of work with the given configuration
|
||||
///
|
||||
/// Ref: https://bitmessage.org/wiki/Proof_of_work
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - data: The message data
|
||||
/// - pubKey: The message recipient
|
||||
/// - timestamp: The timestamp
|
||||
/// - ttl: The message time to live in milliseconds
|
||||
/// - Returns: A nonce string or `nil` if it failed
|
||||
public static func calculate(data: String, pubKey: String, timestamp: UInt64, ttl: UInt64) -> String? {
|
||||
let payload = createPayload(pubKey: pubKey, data: data, timestamp: timestamp, ttl: ttl)
|
||||
let target = calcTarget(ttl: ttl, payloadLength: payload.count, nonceTrials: Int(SnodeAPI.powDifficulty))
|
||||
|
||||
// Start with the max value
|
||||
var trialValue = UInt64.max
|
||||
|
||||
let initialHash = payload.sha512()
|
||||
var nonce = [UInt8](repeating: 0, count: nonceLength)
|
||||
|
||||
while trialValue > target {
|
||||
nonce = nonce.increment(by: 1)
|
||||
|
||||
// This is different to the bitmessage PoW
|
||||
// resultHash = hash(nonce + hash(data)) ==> hash(nonce + initialHash)
|
||||
let resultHash = (nonce + initialHash).sha512()
|
||||
let trialValueArray = Array(resultHash[0..<8])
|
||||
trialValue = UInt64(trialValueArray)
|
||||
}
|
||||
|
||||
return nonce.toBase64()
|
||||
}
|
||||
|
||||
/// Get the proof of work payload
|
||||
private static func createPayload(pubKey: String, data: String, timestamp: UInt64, ttl: UInt64) -> [UInt8] {
|
||||
let timestampString = String(timestamp)
|
||||
let ttlString = String(ttl)
|
||||
let payloadString = timestampString + ttlString + pubKey + data
|
||||
return payloadString.bytes
|
||||
}
|
||||
|
||||
/// Calculate the target we need to reach
|
||||
private static func calcTarget(ttl: UInt64, payloadLength: Int, nonceTrials: Int) -> UInt64 {
|
||||
let two16 = UInt64(pow(2, 16) - 1)
|
||||
let two64 = UInt64(pow(2, 64) - 1)
|
||||
|
||||
// Do all the calculations
|
||||
let totalLength = UInt64(payloadLength + nonceLength)
|
||||
let ttlInSeconds = ttl / 1000
|
||||
let ttlMult = ttlInSeconds * totalLength
|
||||
|
||||
// UInt64 values
|
||||
let innerFrac = ttlMult / two16
|
||||
let lenPlusInnerFrac = totalLength + innerFrac
|
||||
let denominator = UInt64(nonceTrials) * lenPlusInnerFrac
|
||||
|
||||
return two64 / denominator
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
public struct LokiMessage {
|
||||
/// The hex encoded public key of the recipient.
|
||||
let recipientPublicKey: String
|
||||
/// The content of the message.
|
||||
let data: LosslessStringConvertible
|
||||
/// The time to live for the message in milliseconds.
|
||||
let ttl: UInt64
|
||||
/// Whether this message is a ping.
|
||||
///
|
||||
/// - Note: The concept of pinging only applies to P2P messaging.
|
||||
let isPing: Bool
|
||||
/// When the proof of work was calculated, if applicable (P2P messages don't require proof of work).
|
||||
///
|
||||
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
|
||||
private(set) var timestamp: UInt64? = nil
|
||||
/// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work).
|
||||
private(set) var nonce: String? = nil
|
||||
|
||||
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) {
|
||||
self.recipientPublicKey = destination
|
||||
self.data = data
|
||||
self.ttl = ttl
|
||||
self.isPing = isPing
|
||||
}
|
||||
|
||||
/// Construct a `LokiMessage` from a `SignalMessage`.
|
||||
///
|
||||
/// - Note: `timestamp` is the original message timestamp (i.e. `TSOutgoingMessage.timestamp`).
|
||||
public static func from(signalMessage: SignalMessage) -> LokiMessage? {
|
||||
// To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object
|
||||
do {
|
||||
let wrappedMessage = try MessageWrapper.wrap(message: signalMessage)
|
||||
let data = wrappedMessage.base64EncodedString()
|
||||
let destination = signalMessage.recipientPublicKey
|
||||
var ttl = TTLUtilities.fallbackMessageTTL
|
||||
if let messageTTL = signalMessage.ttl, messageTTL > 0 { ttl = UInt64(messageTTL) }
|
||||
let isPing = signalMessage.isPing
|
||||
return LokiMessage(destination: destination, data: data, ttl: ttl, isPing: isPing)
|
||||
} catch let error {
|
||||
print("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the proof of work for this message.
|
||||
///
|
||||
/// - Returns: The promise of a new message with its `timestamp` and `nonce` set.
|
||||
public func calculatePoW() -> Promise<LokiMessage> {
|
||||
return Promise<LokiMessage> { seal in
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let now = NSDate.ows_millisecondTimeStamp()
|
||||
let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented
|
||||
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.recipientPublicKey, timestamp: now, ttl: self.ttl) {
|
||||
var result = self
|
||||
result.timestamp = now
|
||||
result.nonce = nonce
|
||||
seal.fulfill(result)
|
||||
} else {
|
||||
seal.reject(SnodeAPI.SnodeAPIError.proofOfWorkCalculationFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func toJSON() -> JSON {
|
||||
var result = [ "pubKey" : recipientPublicKey, "data" : data.description, "ttl" : String(ttl) ]
|
||||
if let timestamp = timestamp, let nonce = nonce {
|
||||
result["timestamp"] = String(timestamp)
|
||||
result["nonce"] = nonce
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
|
||||
public enum MessageWrapper {
|
||||
|
||||
public enum Error : LocalizedError {
|
||||
case failedToWrapData
|
||||
case failedToWrapMessageInEnvelope
|
||||
case failedToWrapEnvelopeInWebSocketMessage
|
||||
case failedToUnwrapData
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .failedToWrapData: return "Failed to wrap data."
|
||||
case .failedToWrapMessageInEnvelope: return "Failed to wrap message in envelope."
|
||||
case .failedToWrapEnvelopeInWebSocketMessage: return "Failed to wrap envelope in web socket message."
|
||||
case .failedToUnwrapData: return "Failed to unwrap data."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps `message` in an `SSKProtoEnvelope` and then a `WebSocketProtoWebSocketMessage` to match the desktop application.
|
||||
public static func wrap(message: SignalMessage) throws -> Data {
|
||||
do {
|
||||
let envelope = try createEnvelope(around: message)
|
||||
let webSocketMessage = try createWebSocketMessage(around: envelope)
|
||||
return try webSocketMessage.serializedData()
|
||||
} catch let error {
|
||||
throw error as? Error ?? Error.failedToWrapData
|
||||
}
|
||||
}
|
||||
|
||||
private static func createEnvelope(around message: SignalMessage) throws -> SSKProtoEnvelope {
|
||||
do {
|
||||
let builder = SSKProtoEnvelope.builder(type: message.type, timestamp: message.timestamp)
|
||||
builder.setSource(message.senderPublicKey)
|
||||
builder.setSourceDevice(message.senderDeviceID)
|
||||
if let content = try Data(base64Encoded: message.content, options: .ignoreUnknownCharacters) {
|
||||
builder.setContent(content)
|
||||
} else {
|
||||
throw Error.failedToWrapMessageInEnvelope
|
||||
}
|
||||
return try builder.build()
|
||||
} catch let error {
|
||||
print("[Loki] Failed to wrap message in envelope: \(error).")
|
||||
throw Error.failedToWrapMessageInEnvelope
|
||||
}
|
||||
}
|
||||
|
||||
private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage {
|
||||
do {
|
||||
let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1..<UInt64.max))
|
||||
requestBuilder.setBody(try envelope.serializedData())
|
||||
let messageBuilder = WebSocketProtoWebSocketMessage.builder(type: .request)
|
||||
messageBuilder.setRequest(try requestBuilder.build())
|
||||
return try messageBuilder.build()
|
||||
} catch let error {
|
||||
print("[Loki] Failed to wrap envelope in web socket message: \(error).")
|
||||
throw Error.failedToWrapEnvelopeInWebSocketMessage
|
||||
}
|
||||
}
|
||||
|
||||
/// - Note: `data` shouldn't be base 64 encoded.
|
||||
public static func unwrap(data: Data) throws -> SSKProtoEnvelope {
|
||||
do {
|
||||
let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data)
|
||||
let envelope = webSocketMessage.request!.body!
|
||||
return try SSKProtoEnvelope.parseData(envelope)
|
||||
} catch let error {
|
||||
print("[Loki] Failed to unwrap data: \(error).")
|
||||
throw Error.failedToUnwrapData
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
|
||||
extension OnionRequestAPI {
|
||||
|
||||
internal static func encode(ciphertext: Data, json: JSON) throws -> Data {
|
||||
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
|
||||
guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON }
|
||||
let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ])
|
||||
let ciphertextSize = Int32(ciphertext.count).littleEndian
|
||||
let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout<Int32>.size) }
|
||||
return ciphertextSizeAsData + ciphertext + jsonAsData
|
||||
}
|
||||
|
||||
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
|
||||
internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<EncryptionResult> {
|
||||
let (promise, seal) = Promise<EncryptionResult>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
do {
|
||||
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
// Wrapping isn't needed for file server or open group onion requests
|
||||
switch destination {
|
||||
case .snode(let snode):
|
||||
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
|
||||
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||
let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
|
||||
let result = try EncryptionUtilities.encrypt(plaintext, using: snodeX25519PublicKey)
|
||||
seal.fulfill(result)
|
||||
case .server(_, let serverX25519PublicKey):
|
||||
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||
let result = try EncryptionUtilities.encrypt(plaintext, using: serverX25519PublicKey)
|
||||
seal.fulfill(result)
|
||||
}
|
||||
} catch (let error) {
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
|
||||
internal static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
|
||||
let (promise, seal) = Promise<EncryptionResult>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
var parameters: JSON
|
||||
switch rhs {
|
||||
case .snode(let snode):
|
||||
guard let snodeED25519PublicKey = snode.publicKeySet?.ed25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
|
||||
parameters = [ "destination" : snodeED25519PublicKey ]
|
||||
case .server(let host, _):
|
||||
parameters = [ "host" : host, "target" : "/loki/v2/lsrpc", "method" : "POST" ]
|
||||
}
|
||||
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
|
||||
let x25519PublicKey: String
|
||||
switch lhs {
|
||||
case .snode(let snode):
|
||||
guard let snodeX25519PublicKey = snode.publicKeySet?.x25519Key else { return seal.reject(Error.snodePublicKeySetMissing) }
|
||||
x25519PublicKey = snodeX25519PublicKey
|
||||
case .server(_, let serverX25519PublicKey):
|
||||
x25519PublicKey = serverX25519PublicKey
|
||||
}
|
||||
do {
|
||||
let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
|
||||
let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey)
|
||||
seal.fulfill(result)
|
||||
} catch (let error) {
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
|
@ -1,435 +0,0 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
|
||||
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
|
||||
public enum OnionRequestAPI {
|
||||
private static var pathFailureCount: [Path:UInt] = [:]
|
||||
private static var snodeFailureCount: [Snode:UInt] = [:]
|
||||
public static var guardSnodes: Set<Snode> = []
|
||||
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
|
||||
|
||||
// MARK: Settings
|
||||
/// The number of snodes (including the guard snode) in a path.
|
||||
private static let pathSize: UInt = 3
|
||||
/// The number of times a path can fail before it's replaced.
|
||||
private static let pathFailureThreshold: UInt = 3
|
||||
/// The number of times a snode can fail before it's replaced.
|
||||
private static let snodeFailureThreshold: UInt = 3
|
||||
/// The number of paths to maintain.
|
||||
public static let targetPathCount: UInt = 2
|
||||
|
||||
/// The number of guard snodes required to maintain `targetPathCount` paths.
|
||||
private static var targetGuardSnodeCount: UInt { return targetPathCount } // One per path
|
||||
|
||||
// MARK: Destination
|
||||
internal enum Destination {
|
||||
case snode(Snode)
|
||||
case server(host: String, x25519PublicKey: String)
|
||||
}
|
||||
|
||||
// MARK: Error
|
||||
public enum Error : LocalizedError {
|
||||
case httpRequestFailedAtDestination(statusCode: UInt, json: JSON)
|
||||
case insufficientSnodes
|
||||
case invalidURL
|
||||
case missingSnodeVersion
|
||||
case snodePublicKeySetMissing
|
||||
case unsupportedSnodeVersion(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .httpRequestFailedAtDestination(let statusCode): return "HTTP request failed at destination with status code: \(statusCode)."
|
||||
case .insufficientSnodes: return "Couldn't find enough snodes to build a path."
|
||||
case .invalidURL: return "Invalid URL"
|
||||
case .missingSnodeVersion: return "Missing snode version."
|
||||
case .snodePublicKeySetMissing: return "Missing snode public key set."
|
||||
case .unsupportedSnodeVersion(let version): return "Unsupported snode version: \(version)."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Path
|
||||
public typealias Path = [Snode]
|
||||
|
||||
// MARK: Onion Building Result
|
||||
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, destinationSymmetricKey: Data)
|
||||
|
||||
// MARK: Private API
|
||||
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
|
||||
private static func testSnode(_ snode: Snode) -> Promise<Void> {
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let url = "\(snode.address):\(snode.port)/get_stats/v1"
|
||||
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
|
||||
HTTP.execute(.get, url, timeout: timeout).done2 { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
|
||||
if version >= "2.0.7" {
|
||||
seal.fulfill(())
|
||||
} else {
|
||||
print("[Loki] [Onion Request API] Unsupported snode version: \(version).")
|
||||
seal.reject(Error.unsupportedSnodeVersion(version))
|
||||
}
|
||||
}.catch2 { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes`
|
||||
/// if not enough (reliable) snodes are available.
|
||||
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
|
||||
if guardSnodes.count >= targetGuardSnodeCount {
|
||||
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
|
||||
} else {
|
||||
print("[Loki] [Onion Request API] Populating guard snode cache.")
|
||||
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<Set<Snode>> in // Just used to populate the snode pool
|
||||
var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue
|
||||
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
|
||||
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { throw Error.insufficientSnodes }
|
||||
func getGuardSnode() -> Promise<Snode> {
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
guard let candidate = unusedSnodes.randomElement() else { return Promise<Snode> { $0.reject(Error.insufficientSnodes) } }
|
||||
unusedSnodes.remove(candidate) // All used snodes should be unique
|
||||
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
|
||||
// Loop until a reliable guard snode is found
|
||||
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
|
||||
withDelay(0.1, completionQueue: SnodeAPI.workQueue) { getGuardSnode() }
|
||||
}
|
||||
}
|
||||
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
|
||||
return when(fulfilled: promises).map2 { guardSnodes in
|
||||
let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
|
||||
OnionRequestAPI.guardSnodes = guardSnodesAsSet
|
||||
return guardSnodesAsSet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
|
||||
/// if not enough (reliable) snodes are available.
|
||||
private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
|
||||
print("[Loki] [Onion Request API] Building onion request paths.")
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .buildingPaths, object: nil)
|
||||
}
|
||||
return SnodeAPI.getRandomSnode().then2 { _ -> Promise<[Path]> in // Just used to populate the snode pool
|
||||
let reusableGuardSnodes = reusablePaths.map { $0[0] }
|
||||
return getGuardSnodes(reusing: reusableGuardSnodes).map2 { guardSnodes -> [Path] in
|
||||
var unusedSnodes = SnodeAPI.snodePool.subtracting(guardSnodes).subtracting(reusablePaths.flatMap { $0 })
|
||||
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
|
||||
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
|
||||
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
|
||||
// Don't test path snodes as this would reveal the user's IP to them
|
||||
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
|
||||
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
|
||||
unusedSnodes.remove(pathSnode) // All used snodes should be unique
|
||||
return pathSnode
|
||||
}
|
||||
print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).")
|
||||
return result
|
||||
}
|
||||
}.map2 { paths in
|
||||
OnionRequestAPI.paths = paths + reusablePaths
|
||||
Storage.writeSync { transaction in
|
||||
print("[Loki] Persisting onion request paths to database.")
|
||||
Storage.setOnionRequestPaths(paths, using: transaction)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
|
||||
private static func getPath(excluding snode: Snode?) -> Promise<Path> {
|
||||
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
|
||||
var paths = OnionRequestAPI.paths
|
||||
if paths.isEmpty {
|
||||
paths = Storage.getOnionRequestPaths()
|
||||
OnionRequestAPI.paths = paths
|
||||
if !paths.isEmpty {
|
||||
guardSnodes.formUnion([ paths[0][0] ])
|
||||
if paths.count >= 2 {
|
||||
guardSnodes.formUnion([ paths[1][0] ])
|
||||
}
|
||||
}
|
||||
}
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
if paths.count >= targetPathCount {
|
||||
if let snode = snode {
|
||||
return Promise { $0.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) }
|
||||
} else {
|
||||
return Promise { $0.fulfill(paths.randomElement()!) }
|
||||
}
|
||||
} else if !paths.isEmpty {
|
||||
if let snode = snode {
|
||||
if let path = paths.first(where: { !$0.contains(snode) }) {
|
||||
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
|
||||
return Promise { $0.fulfill(path) }
|
||||
} else {
|
||||
return buildPaths(reusing: paths).map2 { paths in
|
||||
return paths.filter { !$0.contains(snode) }.randomElement()!
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buildPaths(reusing: paths).retainUntilComplete() // Re-build paths in the background
|
||||
return Promise { $0.fulfill(paths.randomElement()!) }
|
||||
}
|
||||
} else {
|
||||
return buildPaths(reusing: []).map2 { paths in
|
||||
if let snode = snode {
|
||||
return paths.filter { !$0.contains(snode) }.randomElement()!
|
||||
} else {
|
||||
return paths.randomElement()!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func dropGuardSnode(_ snode: Snode) {
|
||||
guardSnodes = guardSnodes.filter { $0 != snode }
|
||||
}
|
||||
|
||||
private static func drop(_ snode: Snode) throws {
|
||||
// We repair the path here because we can do it sync. In the case where we drop a whole
|
||||
// path we leave the re-building up to getPath(excluding:) because re-building the path
|
||||
// in that case is async.
|
||||
OnionRequestAPI.snodeFailureCount[snode] = 0
|
||||
var oldPaths = paths
|
||||
guard let pathIndex = oldPaths.firstIndex(where: { $0.contains(snode) }) else { return }
|
||||
var path = oldPaths[pathIndex]
|
||||
guard let snodeIndex = path.firstIndex(of: snode) else { return }
|
||||
path.remove(at: snodeIndex)
|
||||
let unusedSnodes = SnodeAPI.snodePool.subtracting(oldPaths.flatMap { $0 })
|
||||
guard !unusedSnodes.isEmpty else { throw Error.insufficientSnodes }
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
path.append(unusedSnodes.randomElement()!)
|
||||
// Don't test the new snode as this would reveal the user's IP
|
||||
oldPaths.remove(at: pathIndex)
|
||||
let newPaths = oldPaths + [ path ]
|
||||
paths = newPaths
|
||||
Storage.writeSync { transaction in
|
||||
print("[Loki] Persisting onion request paths to database.")
|
||||
Storage.setOnionRequestPaths(newPaths, using: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
private static func drop(_ path: Path) {
|
||||
OnionRequestAPI.pathFailureCount[path] = 0
|
||||
var paths = OnionRequestAPI.paths
|
||||
guard let pathIndex = paths.firstIndex(of: path) else { return }
|
||||
paths.remove(at: pathIndex)
|
||||
OnionRequestAPI.paths = paths
|
||||
Storage.writeSync { transaction in
|
||||
if !paths.isEmpty {
|
||||
print("[Loki] Persisting onion request paths to database.")
|
||||
Storage.setOnionRequestPaths(paths, using: transaction)
|
||||
} else {
|
||||
Storage.clearOnionRequestPaths(using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an onion around `payload` and returns the result.
|
||||
private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise<OnionBuildingResult> {
|
||||
var guardSnode: Snode!
|
||||
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
|
||||
var encryptionResult: EncryptionResult!
|
||||
var snodeToExclude: Snode?
|
||||
if case .snode(let snode) = destination { snodeToExclude = snode }
|
||||
return getPath(excluding: snodeToExclude).then2 { path -> Promise<EncryptionResult> in
|
||||
guardSnode = path.first!
|
||||
// Encrypt in reverse order, i.e. the destination first
|
||||
return encrypt(payload, for: destination).then2 { r -> Promise<EncryptionResult> in
|
||||
targetSnodeSymmetricKey = r.symmetricKey
|
||||
// Recursively encrypt the layers of the onion (again in reverse order)
|
||||
encryptionResult = r
|
||||
var path = path
|
||||
var rhs = destination
|
||||
func addLayer() -> Promise<EncryptionResult> {
|
||||
if path.isEmpty {
|
||||
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
|
||||
} else {
|
||||
let lhs = Destination.snode(path.removeLast())
|
||||
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
|
||||
encryptionResult = r
|
||||
rhs = lhs
|
||||
return addLayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
return addLayer()
|
||||
}
|
||||
}.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
|
||||
}
|
||||
|
||||
// MARK: Internal API
|
||||
/// Sends an onion request to `snode`. Builds new paths as needed.
|
||||
internal static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> {
|
||||
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
||||
return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise<JSON> in
|
||||
guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json) = error else { throw error }
|
||||
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends an onion request to `server`. Builds new paths as needed.
|
||||
internal static func sendOnionRequest(_ request: NSURLRequest, to server: String, using x25519PublicKey: String, isJSONRequired: Bool = true) -> Promise<JSON> {
|
||||
var rawHeaders = request.allHTTPHeaderFields ?? [:]
|
||||
rawHeaders.removeValue(forKey: "User-Agent")
|
||||
var headers: JSON = rawHeaders.mapValues { value in
|
||||
switch value.lowercased() {
|
||||
case "true": return true
|
||||
case "false": return false
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
guard let url = request.url?.absoluteString, let host = request.url?.host else { return Promise(error: Error.invalidURL) }
|
||||
var endpoint = ""
|
||||
if server.count < url.count {
|
||||
guard let serverEndIndex = url.range(of: server)?.upperBound else { return Promise(error: Error.invalidURL) }
|
||||
let endpointStartIndex = url.index(after: serverEndIndex)
|
||||
endpoint = String(url[endpointStartIndex..<url.endIndex])
|
||||
}
|
||||
let parametersAsString: String
|
||||
if let tsRequest = request as? TSRequest {
|
||||
headers["Content-Type"] = "application/json"
|
||||
let tsRequestParameters = tsRequest.parameters
|
||||
if !tsRequestParameters.isEmpty {
|
||||
guard let parameters = try? JSONSerialization.data(withJSONObject: tsRequestParameters, options: [ .fragmentsAllowed ]) else {
|
||||
return Promise(error: HTTP.Error.invalidJSON)
|
||||
}
|
||||
parametersAsString = String(bytes: parameters, encoding: .utf8) ?? "null"
|
||||
} else {
|
||||
parametersAsString = "null"
|
||||
}
|
||||
} else {
|
||||
headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"]
|
||||
if let parametersAsInputStream = request.httpBodyStream, let parameters = try? Data(from: parametersAsInputStream) {
|
||||
parametersAsString = "{ \"fileUpload\" : \"\(String(data: parameters.base64EncodedData(), encoding: .utf8) ?? "null")\" }"
|
||||
} else {
|
||||
parametersAsString = "null"
|
||||
}
|
||||
}
|
||||
let payload: JSON = [
|
||||
"body" : parametersAsString,
|
||||
"endpoint" : endpoint,
|
||||
"method" : request.httpMethod!,
|
||||
"headers" : headers
|
||||
]
|
||||
let destination = Destination.server(host: host, x25519PublicKey: x25519PublicKey)
|
||||
let promise = sendOnionRequest(with: payload, to: destination, isJSONRequired: isJSONRequired)
|
||||
promise.catch2 { error in
|
||||
print("[Loki] [Onion Request API] Couldn't reach server: \(url) due to error: \(error).")
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
internal static func sendOnionRequest(with payload: JSON, to destination: Destination, isJSONRequired: Bool = true) -> Promise<JSON> {
|
||||
let (promise, seal) = Promise<JSON>.pending()
|
||||
var guardSnode: Snode!
|
||||
SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths`
|
||||
buildOnion(around: payload, targetedAt: destination).done2 { intermediate in
|
||||
guardSnode = intermediate.guardSnode
|
||||
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req/v2"
|
||||
let finalEncryptionResult = intermediate.finalEncryptionResult
|
||||
let onion = finalEncryptionResult.ciphertext
|
||||
if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) {
|
||||
print("[Loki] Approaching request size limit: ~\(onion.count) bytes.")
|
||||
}
|
||||
let parameters: JSON = [
|
||||
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
|
||||
]
|
||||
let body: Data
|
||||
do {
|
||||
body = try encode(ciphertext: onion, json: parameters)
|
||||
} catch {
|
||||
return seal.reject(error)
|
||||
}
|
||||
let destinationSymmetricKey = intermediate.destinationSymmetricKey
|
||||
HTTP.execute(.post, url, body: body).done2 { rawResponse in
|
||||
guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String,
|
||||
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= EncryptionUtilities.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
do {
|
||||
let data = try DecryptionUtilities.decrypt(ivAndCiphertext, usingAESGCMWithSymmetricKey: destinationSymmetricKey)
|
||||
guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON,
|
||||
let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
if statusCode == 406 { // Clock out of sync
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
seal.reject(SnodeAPI.SnodeAPIError.clockOutOfSync)
|
||||
} else if let bodyAsString = json["body"] as? String {
|
||||
let body: JSON
|
||||
if !isJSONRequired {
|
||||
body = [ "result" : bodyAsString ]
|
||||
} else {
|
||||
guard let bodyAsData = bodyAsString.data(using: .utf8),
|
||||
let b = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||
body = b
|
||||
}
|
||||
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body)) }
|
||||
seal.fulfill(body)
|
||||
} else {
|
||||
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json)) }
|
||||
seal.fulfill(json)
|
||||
}
|
||||
} catch {
|
||||
seal.reject(error)
|
||||
}
|
||||
}.catch2 { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}.catch2 { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
promise.catch2 { error in // Must be invoked on LokiAPI.workQueue
|
||||
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { return }
|
||||
let path = paths.first { $0.contains(guardSnode) }
|
||||
func handleUnspecificError() {
|
||||
guard let path = path else { return }
|
||||
var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0
|
||||
pathFailureCount += 1
|
||||
if pathFailureCount >= pathFailureThreshold {
|
||||
dropGuardSnode(guardSnode)
|
||||
path.forEach { snode in
|
||||
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
|
||||
}
|
||||
drop(path)
|
||||
} else {
|
||||
OnionRequestAPI.pathFailureCount[path] = pathFailureCount
|
||||
}
|
||||
}
|
||||
let prefix = "Next node not found: "
|
||||
if let message = json?["result"] as? String, message.hasPrefix(prefix) {
|
||||
let ed25519PublicKey = message.substring(from: prefix.count)
|
||||
if let path = path, let snode = path.first(where: { $0.publicKeySet?.ed25519Key == ed25519PublicKey }) {
|
||||
var snodeFailureCount = OnionRequestAPI.snodeFailureCount[snode] ?? 0
|
||||
snodeFailureCount += 1
|
||||
if snodeFailureCount >= snodeFailureThreshold {
|
||||
SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw
|
||||
do {
|
||||
try drop(snode)
|
||||
} catch {
|
||||
handleUnspecificError()
|
||||
}
|
||||
} else {
|
||||
OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount
|
||||
}
|
||||
} else {
|
||||
handleUnspecificError()
|
||||
}
|
||||
} else if let message = json?["result"] as? String, message == "Loki Server error" {
|
||||
// Do nothing
|
||||
} else {
|
||||
handleUnspecificError()
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
|
||||
@objc(LKSignalMessage)
|
||||
public final class SignalMessage : NSObject {
|
||||
@objc public let type: SSKProtoEnvelope.SSKProtoEnvelopeType
|
||||
@objc public let timestamp: UInt64
|
||||
@objc public let senderPublicKey: String
|
||||
@objc public let senderDeviceID: UInt32
|
||||
@objc public let content: String
|
||||
@objc public let recipientPublicKey: String
|
||||
@objc(ttl)
|
||||
public let objc_ttl: UInt64
|
||||
@objc public let isPing: Bool
|
||||
|
||||
public var ttl: UInt64? { return objc_ttl != 0 ? objc_ttl : nil }
|
||||
|
||||
@objc public init(type: SSKProtoEnvelope.SSKProtoEnvelopeType, timestamp: UInt64, senderID: String, senderDeviceID: UInt32,
|
||||
content: String, recipientID: String, ttl: UInt64, isPing: Bool) {
|
||||
self.type = type
|
||||
self.timestamp = timestamp
|
||||
self.senderPublicKey = senderID
|
||||
self.senderDeviceID = senderDeviceID
|
||||
self.content = content
|
||||
self.recipientPublicKey = recipientID
|
||||
self.objc_ttl = ttl
|
||||
self.isPing = isPing
|
||||
super.init()
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
|
||||
public final class Snode : NSObject, NSCoding {
|
||||
public let address: String
|
||||
public let port: UInt16
|
||||
internal let publicKeySet: KeySet?
|
||||
|
||||
public var ip: String {
|
||||
String(address[address.index(address.startIndex, offsetBy: 8)..<address.endIndex])
|
||||
}
|
||||
|
||||
// MARK: Nested Types
|
||||
internal enum Method : String {
|
||||
/// Only supported by snode targets.
|
||||
case getSwarm = "get_snodes_for_pubkey"
|
||||
/// Only supported by snode targets.
|
||||
case getMessages = "retrieve"
|
||||
case sendMessage = "store"
|
||||
}
|
||||
|
||||
internal struct KeySet {
|
||||
let ed25519Key: String
|
||||
let x25519Key: String
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
internal init(address: String, port: UInt16, publicKeySet: KeySet?) {
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.publicKeySet = publicKeySet
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public init?(coder: NSCoder) {
|
||||
address = coder.decodeObject(forKey: "address") as! String
|
||||
port = coder.decodeObject(forKey: "port") as! UInt16
|
||||
if let idKey = coder.decodeObject(forKey: "idKey") as? String, let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String {
|
||||
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
|
||||
} else {
|
||||
publicKeySet = nil
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(address, forKey: "address")
|
||||
coder.encode(port, forKey: "port")
|
||||
if let keySet = publicKeySet {
|
||||
coder.encode(keySet.ed25519Key, forKey: "idKey")
|
||||
coder.encode(keySet.x25519Key, forKey: "encryptionKey")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? Snode else { return false }
|
||||
return address == other.address && port == other.port
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return address.hashValue ^ port.hashValue
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String { return "\(address):\(port)" }
|
||||
}
|
|
@ -1,352 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKSnodeAPI)
|
||||
public final class SnodeAPI : NSObject {
|
||||
internal static let workQueue = DispatchQueue(label: "SnodeAPI.workQueue", qos: .userInitiated) // It's important that this is a serial queue
|
||||
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodeFailureCount: [Snode:UInt] = [:]
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var snodePool: Set<Snode> = []
|
||||
/// - Note: Should only be accessed from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static var swarmCache: [String:[Snode]] = [:]
|
||||
|
||||
internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() }
|
||||
|
||||
// MARK: Settings
|
||||
private static let maxRetryCount: UInt = 4
|
||||
private static let minimumSnodePoolCount = 64
|
||||
private static let minimumSwarmSnodeCount = 2
|
||||
private static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
|
||||
private static let snodeFailureThreshold = 4
|
||||
private static let targetSwarmSnodeCount = 2
|
||||
|
||||
internal static var powDifficulty: UInt = 1
|
||||
/// - Note: Changing this on the fly is not recommended.
|
||||
internal static var useOnionRequests = true
|
||||
|
||||
// MARK: Error
|
||||
@objc(LKSnodeAPIError)
|
||||
public class SnodeAPIError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let proofOfWorkCalculationFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Failed to calculate proof of work." ])
|
||||
@objc public static let messageConversionFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Failed to construct message." ])
|
||||
@objc public static let clockOutOfSync = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Your clock is out of sync with the service node network." ])
|
||||
@objc public static let randomSnodePoolUpdatingFailed = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Failed to update random service node pool." ])
|
||||
@objc public static let missingSnodeVersion = SnodeAPIError(domain: "LokiAPIErrorDomain", code: 5, userInfo: [ NSLocalizedDescriptionKey : "Missing service node version." ])
|
||||
}
|
||||
|
||||
// MARK: Type Aliases
|
||||
public typealias MessageListPromise = Promise<[SSKProtoEnvelope]>
|
||||
public typealias RawResponse = Any
|
||||
public typealias RawResponsePromise = Promise<RawResponse>
|
||||
|
||||
// MARK: Lifecycle
|
||||
override private init() { }
|
||||
|
||||
// MARK: Core
|
||||
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
|
||||
if useOnionRequests {
|
||||
return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any }
|
||||
} else {
|
||||
let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
|
||||
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in
|
||||
guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error else { throw error }
|
||||
throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getRandomSnode() -> Promise<Snode> {
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
snodePool = storage.getSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
if snodePool.count < minimumSnodePoolCount {
|
||||
let target = seedNodePool.randomElement()!
|
||||
let url = "\(target)/json_rpc"
|
||||
let parameters: JSON = [
|
||||
"method" : "get_n_service_nodes",
|
||||
"params" : [
|
||||
"active_only" : true,
|
||||
"fields" : [
|
||||
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
|
||||
]
|
||||
]
|
||||
]
|
||||
print("[Loki] Populating snode pool using: \(target).")
|
||||
let (promise, seal) = Promise<Snode>.pending()
|
||||
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
|
||||
HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Snode in
|
||||
guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw SnodeAPIError.randomSnodePoolUpdatingFailed }
|
||||
snodePool = try Set(rawSnodes.flatMap { rawSnode in
|
||||
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
|
||||
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawSnode).")
|
||||
return nil
|
||||
}
|
||||
return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
})
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
if !snodePool.isEmpty {
|
||||
return snodePool.randomElement()!
|
||||
} else {
|
||||
throw SnodeAPIError.randomSnodePoolUpdatingFailed
|
||||
}
|
||||
}
|
||||
}.done2 { snode in
|
||||
seal.fulfill(snode)
|
||||
Storage.writeSync { transaction in
|
||||
print("[Loki] Persisting snode pool to database.")
|
||||
storage.setSnodePool(SnodeAPI.snodePool, in: transaction)
|
||||
}
|
||||
}.catch2 { error in
|
||||
print("[Loki] Failed to contact seed node at: \(target).")
|
||||
seal.reject(error)
|
||||
}
|
||||
return promise
|
||||
} else {
|
||||
return Promise<Snode> { seal in
|
||||
// randomElement() uses the system's default random generator, which is cryptographically secure
|
||||
seal.fulfill(snodePool.randomElement()!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getSwarm(for publicKey: String, isForcedReload: Bool = false) -> Promise<[Snode]> {
|
||||
if swarmCache[publicKey] == nil {
|
||||
storage.dbReadConnection.read { transaction in
|
||||
swarmCache[publicKey] = storage.getSwarm(for: publicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
|
||||
return Promise<[Snode]> { $0.fulfill(cachedSwarm) }
|
||||
} else {
|
||||
print("[Loki] Getting swarm for: \(publicKey == getUserHexEncodedPublicKey() ? "self" : publicKey).")
|
||||
let parameters: [String:Any] = [ "pubKey" : publicKey ]
|
||||
return getRandomSnode().then2 { snode in
|
||||
attempt(maxRetryCount: 4, recoveringOn: SnodeAPI.workQueue) {
|
||||
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||
}
|
||||
}.map2 { rawSnodes in
|
||||
let swarm = parseSnodes(from: rawSnodes)
|
||||
swarmCache[publicKey] = swarm
|
||||
Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: publicKey, in: transaction)
|
||||
}
|
||||
return swarm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> {
|
||||
// shuffled() uses the system's default random generator, which is cryptographically secure
|
||||
return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) }
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSnodePool(_ snode: Snode) {
|
||||
SnodeAPI.snodePool.remove(snode)
|
||||
Storage.writeSync { transaction in
|
||||
storage.dropSnodeFromSnodePool(snode, in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public static func clearSnodePool() {
|
||||
snodePool.removeAll()
|
||||
Storage.writeSync { transaction in
|
||||
storage.clearSnodePool(in: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
internal static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) {
|
||||
let swarm = SnodeAPI.swarmCache[publicKey]
|
||||
if var swarm = swarm, let index = swarm.firstIndex(of: snode) {
|
||||
swarm.remove(at: index)
|
||||
SnodeAPI.swarmCache[publicKey] = swarm
|
||||
Storage.writeSync { transaction in
|
||||
storage.setSwarm(swarm, for: publicKey, in: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Receiving
|
||||
internal static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise {
|
||||
Storage.writeSync { transaction in
|
||||
Storage.pruneLastMessageHashInfoIfExpired(for: snode, associatedWith: publicKey, using: transaction)
|
||||
}
|
||||
let lastHash = Storage.getLastMessageHash(for: snode, associatedWith: publicKey) ?? ""
|
||||
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
|
||||
return invoke(.getMessages, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||
}
|
||||
|
||||
public static func getMessages(for publicKey: String) -> Promise<Set<MessageListPromise>> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
|
||||
getTargetSnodes(for: publicKey).mapValues2 { targetSnode in
|
||||
getRawMessages(from: targetSnode, associatedWith: publicKey).map2 {
|
||||
parseRawMessagesResponse($0, from: targetSnode, associatedWith: publicKey)
|
||||
}
|
||||
}.map2 { Set($0) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Sending
|
||||
@objc(sendSignalMessage:)
|
||||
public static func objc_sendSignalMessage(_ signalMessage: SignalMessage) -> AnyPromise {
|
||||
let promise = sendSignalMessage(signalMessage).mapValues2 { AnyPromise.from($0) }.map2 { Set($0) }
|
||||
return AnyPromise.from(promise)
|
||||
}
|
||||
|
||||
public static func sendSignalMessage(_ signalMessage: SignalMessage) -> Promise<Set<RawResponsePromise>> {
|
||||
// Convert the message to a Loki message
|
||||
guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: SnodeAPIError.messageConversionFailed) }
|
||||
let publicKey = lokiMessage.recipientPublicKey
|
||||
let notificationCenter = NotificationCenter.default
|
||||
notificationCenter.post(name: .calculatingPoW, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Calculate proof of work
|
||||
return lokiMessage.calculatePoW().then2 { lokiMessageWithPoW -> Promise<Set<RawResponsePromise>> in
|
||||
notificationCenter.post(name: .routing, object: NSNumber(value: signalMessage.timestamp))
|
||||
// Get the target snodes
|
||||
return getTargetSnodes(for: publicKey).map2 { snodes in
|
||||
notificationCenter.post(name: .messageSending, object: NSNumber(value: signalMessage.timestamp))
|
||||
let parameters = lokiMessageWithPoW.toJSON()
|
||||
return Set(snodes.map { snode in
|
||||
// Send the message to the target snode
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: SnodeAPI.workQueue) {
|
||||
invoke(.sendMessage, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||
}.map2 { rawResponse in
|
||||
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||
guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return rawResponse }
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
|
||||
}
|
||||
return rawResponse
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Parsing
|
||||
|
||||
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.
|
||||
|
||||
private static func parseSnodes(from rawResponse: Any) -> [Snode] {
|
||||
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
|
||||
print("[Loki] Failed to parse targets from: \(rawResponse).")
|
||||
return []
|
||||
}
|
||||
return rawSnodes.flatMap { rawSnode in
|
||||
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||
print("[Loki] Failed to parse target from: \(rawSnode).")
|
||||
return nil
|
||||
}
|
||||
return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
|
||||
}
|
||||
}
|
||||
|
||||
internal static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [SSKProtoEnvelope] {
|
||||
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
|
||||
updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages)
|
||||
let rawNewMessages = removeDuplicates(from: rawMessages, associatedWith: publicKey)
|
||||
let newMessages = parseProtoEnvelopes(from: rawNewMessages)
|
||||
return newMessages
|
||||
}
|
||||
|
||||
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
|
||||
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
|
||||
Storage.writeSync { transaction in
|
||||
Storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey, to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
|
||||
}
|
||||
} else if (!rawMessages.isEmpty) {
|
||||
print("[Loki] Failed to update last message hash value from: \(rawMessages).")
|
||||
}
|
||||
}
|
||||
|
||||
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
|
||||
var receivedMessages = Storage.getReceivedMessages(for: publicKey) ?? []
|
||||
return rawMessages.filter { rawMessage in
|
||||
guard let hash = rawMessage["hash"] as? String else {
|
||||
print("[Loki] Missing hash value for message: \(rawMessage).")
|
||||
return false
|
||||
}
|
||||
let isDuplicate = receivedMessages.contains(hash)
|
||||
receivedMessages.insert(hash)
|
||||
Storage.writeSync { transaction in
|
||||
Storage.setReceivedMessages(to: receivedMessages, for: publicKey, using: transaction)
|
||||
}
|
||||
return !isDuplicate
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] {
|
||||
return rawMessages.compactMap { rawMessage in
|
||||
guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else {
|
||||
print("[Loki] Failed to decode data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
guard let envelope = try? MessageWrapper.unwrap(data: data) else {
|
||||
print("[Loki] Failed to unwrap data for message: \(rawMessage).")
|
||||
return nil
|
||||
}
|
||||
return envelope
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Error Handling
|
||||
/// - Note: Should only be invoked from `LokiAPI.workQueue` to avoid race conditions.
|
||||
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
|
||||
#if DEBUG
|
||||
assertOnQueue(SnodeAPI.workQueue)
|
||||
#endif
|
||||
func handleBadSnode() {
|
||||
let oldFailureCount = SnodeAPI.snodeFailureCount[snode] ?? 0
|
||||
let newFailureCount = oldFailureCount + 1
|
||||
SnodeAPI.snodeFailureCount[snode] = newFailureCount
|
||||
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
|
||||
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
|
||||
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
|
||||
if let publicKey = publicKey {
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||
}
|
||||
SnodeAPI.dropSnodeFromSnodePool(snode)
|
||||
print("[Loki] Snode pool count: \(snodePool.count).")
|
||||
SnodeAPI.snodeFailureCount[snode] = 0
|
||||
}
|
||||
}
|
||||
switch statusCode {
|
||||
case 0, 400, 500, 503:
|
||||
// The snode is unreachable
|
||||
handleBadSnode()
|
||||
case 406:
|
||||
print("[Loki] The user's clock is out of sync with the service node network.")
|
||||
return SnodeAPI.SnodeAPIError.clockOutOfSync
|
||||
case 421:
|
||||
// The snode isn't associated with the given public key anymore
|
||||
if let publicKey = publicKey {
|
||||
print("[Loki] Invalidating swarm for: \(publicKey).")
|
||||
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||
} else {
|
||||
print("[Loki] Got a 421 without an associated public key.")
|
||||
}
|
||||
case 432:
|
||||
// The proof of work difficulty is too low
|
||||
if let powDifficulty = json?["difficulty"] as? UInt {
|
||||
if powDifficulty < 100 {
|
||||
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
|
||||
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||
} else {
|
||||
handleBadSnode()
|
||||
}
|
||||
} else {
|
||||
print("[Loki] Failed to update proof of work difficulty.")
|
||||
}
|
||||
default:
|
||||
handleBadSnode()
|
||||
print("[Loki] Unhandled response code: \(statusCode).")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import CryptoSwift
|
||||
|
||||
enum DecryptionUtilities {
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
internal static func decrypt(_ ivAndCiphertext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
|
||||
if Thread.isMainThread {
|
||||
#if DEBUG
|
||||
preconditionFailure("It's illegal to call decrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.")
|
||||
#endif
|
||||
}
|
||||
let iv = ivAndCiphertext[0..<Int(EncryptionUtilities.ivSize)]
|
||||
let ciphertext = ivAndCiphertext[Int(EncryptionUtilities.ivSize)...]
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(EncryptionUtilities.gcmTagSize), mode: .combined)
|
||||
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
|
||||
return Data(try aes.decrypt(ciphertext.bytes))
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import CryptoSwift
|
||||
|
||||
internal typealias EncryptionResult = (ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data)
|
||||
|
||||
enum EncryptionUtilities {
|
||||
internal static let gcmTagSize: UInt = 16
|
||||
internal static let ivSize: UInt = 12
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
internal static func encrypt(_ plaintext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
|
||||
if Thread.isMainThread {
|
||||
#if DEBUG
|
||||
preconditionFailure("It's illegal to call encrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.")
|
||||
#endif
|
||||
}
|
||||
let iv = Data.getSecureRandomData(ofSize: ivSize)!
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
|
||||
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
|
||||
let ciphertext = try aes.encrypt(plaintext.bytes)
|
||||
return iv + Data(bytes: ciphertext)
|
||||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
internal static func encrypt(_ plaintext: Data, using hexEncodedX25519PublicKey: String) throws -> EncryptionResult {
|
||||
if Thread.isMainThread {
|
||||
#if DEBUG
|
||||
preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.")
|
||||
#endif
|
||||
}
|
||||
let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
|
||||
let ephemeralKeyPair = Curve25519.generateKeyPair()
|
||||
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
|
||||
let salt = "LOKI"
|
||||
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
||||
let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey))
|
||||
return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey)
|
||||
}
|
||||
}
|
|
@ -1,107 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
public enum HTTP {
|
||||
private static let seedNodeURLSession = URLSession(configuration: .ephemeral)
|
||||
private static let defaultURLSession = URLSession(configuration: .ephemeral, delegate: defaultURLSessionDelegate, delegateQueue: nil)
|
||||
private static let defaultURLSessionDelegate = DefaultURLSessionDelegateImplementation()
|
||||
|
||||
// MARK: Settings
|
||||
public static let timeout: TimeInterval = 10
|
||||
|
||||
// MARK: URL Session Delegate Implementation
|
||||
private final class DefaultURLSessionDelegateImplementation : NSObject, URLSessionDelegate {
|
||||
|
||||
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
||||
// Snode to snode communication uses self-signed certificates but clients can safely ignore this
|
||||
completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Verb
|
||||
public enum Verb : String {
|
||||
case get = "GET"
|
||||
case put = "PUT"
|
||||
case post = "POST"
|
||||
case delete = "DELETE"
|
||||
}
|
||||
|
||||
// MARK: Error
|
||||
public enum Error : LocalizedError {
|
||||
case generic
|
||||
case httpRequestFailed(statusCode: UInt, json: JSON?)
|
||||
case invalidJSON
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .generic: return "An error occurred."
|
||||
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."
|
||||
case .invalidJSON: return "Invalid JSON."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Main
|
||||
public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
|
||||
return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
|
||||
}
|
||||
|
||||
public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
|
||||
if let parameters = parameters {
|
||||
do {
|
||||
guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) }
|
||||
let body = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ])
|
||||
return execute(verb, url, body: body, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
|
||||
} catch (let error) {
|
||||
return Promise(error: error)
|
||||
}
|
||||
} else {
|
||||
return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
|
||||
}
|
||||
}
|
||||
|
||||
public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise<JSON> {
|
||||
var request = URLRequest(url: URL(string: url)!)
|
||||
request.httpMethod = verb.rawValue
|
||||
request.httpBody = body
|
||||
request.timeoutInterval = timeout
|
||||
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
|
||||
let (promise, seal) = Promise<JSON>.pending()
|
||||
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : defaultURLSession
|
||||
let task = urlSession.dataTask(with: request) { data, response, error in
|
||||
guard let data = data, let response = response as? HTTPURLResponse else {
|
||||
if let error = error {
|
||||
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
|
||||
} else {
|
||||
print("[Loki] \(verb.rawValue) request to \(url) failed.")
|
||||
}
|
||||
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
|
||||
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
|
||||
}
|
||||
if let error = error {
|
||||
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
|
||||
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
|
||||
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
|
||||
}
|
||||
let statusCode = UInt(response.statusCode)
|
||||
var json: JSON? = nil
|
||||
if let j = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
|
||||
json = j
|
||||
} else if let result = String(data: data, encoding: .utf8) {
|
||||
json = [ "result" : result ]
|
||||
}
|
||||
guard 200...299 ~= statusCode else {
|
||||
let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided"
|
||||
print("[Loki] \(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
|
||||
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json))
|
||||
}
|
||||
if let json = json {
|
||||
seal.fulfill(json)
|
||||
} else {
|
||||
print("[Loki] Couldn't parse JSON returned by \(verb.rawValue) request to \(url).")
|
||||
return seal.reject(Error.invalidJSON)
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
return promise
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
|
||||
public final class ClosedGroupRatchet : NSObject, NSCoding {
|
||||
public let chainKey: String
|
||||
public let keyIndex: UInt
|
||||
public let messageKeys: [String]
|
||||
|
||||
// MARK: Initialization
|
||||
public init(chainKey: String, keyIndex: UInt, messageKeys: [String]) {
|
||||
self.chainKey = chainKey
|
||||
self.keyIndex = keyIndex
|
||||
self.messageKeys = messageKeys
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public init?(coder: NSCoder) {
|
||||
guard let chainKey = coder.decodeObject(forKey: "chainKey") as? String,
|
||||
let keyIndex = coder.decodeObject(forKey: "keyIndex") as? UInt,
|
||||
let messageKeys = coder.decodeObject(forKey: "messageKeys") as? [String] else { return nil }
|
||||
self.chainKey = chainKey
|
||||
self.keyIndex = UInt(keyIndex)
|
||||
self.messageKeys = messageKeys
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(chainKey, forKey: "chainKey")
|
||||
coder.encode(keyIndex, forKey: "keyIndex")
|
||||
coder.encode(messageKeys, forKey: "messageKeys")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? ClosedGroupRatchet else { return false }
|
||||
return chainKey == other.chainKey && keyIndex == other.keyIndex && messageKeys == other.messageKeys
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return chainKey.hashValue ^ keyIndex.hashValue ^ messageKeys.hashValue
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String { return "[ chainKey : \(chainKey), keyIndex : \(keyIndex), messageKeys : \(messageKeys.prettifiedDescription) ]" }
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
|
||||
internal final class ClosedGroupSenderKey : NSObject, NSCoding {
|
||||
internal let chainKey: Data
|
||||
internal let keyIndex: UInt
|
||||
internal let publicKey: Data
|
||||
|
||||
// MARK: Initialization
|
||||
init(chainKey: Data, keyIndex: UInt, publicKey: Data) {
|
||||
self.chainKey = chainKey
|
||||
self.keyIndex = keyIndex
|
||||
self.publicKey = publicKey
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public init?(coder: NSCoder) {
|
||||
guard let chainKey = coder.decodeObject(forKey: "chainKey") as? Data,
|
||||
let keyIndex = coder.decodeObject(forKey: "keyIndex") as? UInt,
|
||||
let publicKey = coder.decodeObject(forKey: "publicKey") as? Data else { return nil }
|
||||
self.chainKey = chainKey
|
||||
self.keyIndex = UInt(keyIndex)
|
||||
self.publicKey = publicKey
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(chainKey, forKey: "chainKey")
|
||||
coder.encode(keyIndex, forKey: "keyIndex")
|
||||
coder.encode(publicKey, forKey: "publicKey")
|
||||
}
|
||||
|
||||
// MARK: Proto Conversion
|
||||
internal func toProto() throws -> SSKProtoDataMessageClosedGroupUpdateSenderKey {
|
||||
return try SSKProtoDataMessageClosedGroupUpdateSenderKey.builder(chainKey: chainKey, keyIndex: UInt32(keyIndex), publicKey: publicKey).build()
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? ClosedGroupSenderKey else { return false }
|
||||
return chainKey == other.chainKey && keyIndex == other.keyIndex && publicKey == other.publicKey
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return chainKey.hashValue ^ keyIndex.hashValue ^ publicKey.hashValue
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String {
|
||||
return "[ chainKey : \(chainKey), keyIndex : \(keyIndex), publicKey: \(publicKey.toHexString()) ]"
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
|
||||
@objc(LKClosedGroupUpdateMessage)
|
||||
internal final class ClosedGroupUpdateMessage : TSOutgoingMessage {
|
||||
private let kind: Kind
|
||||
|
||||
// MARK: Settings
|
||||
@objc internal override var ttl: UInt32 { return UInt32(TTLUtilities.getTTL(for: .closedGroupUpdate)) }
|
||||
|
||||
@objc internal override func shouldBeSaved() -> Bool { return false }
|
||||
@objc internal override func shouldSyncTranscript() -> Bool { return false }
|
||||
|
||||
// MARK: Kind
|
||||
internal enum Kind {
|
||||
case new(groupPublicKey: Data, name: String, groupPrivateKey: Data, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data])
|
||||
case info(groupPublicKey: Data, name: String, senderKeys: [ClosedGroupSenderKey], members: [Data], admins: [Data])
|
||||
case senderKeyRequest(groupPublicKey: Data)
|
||||
case senderKey(groupPublicKey: Data, senderKey: ClosedGroupSenderKey)
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
internal init(thread: TSThread, kind: Kind) {
|
||||
self.kind = kind
|
||||
super.init(outgoingMessageWithTimestamp: NSDate.ows_millisecondTimeStamp(), in: thread, messageBody: "",
|
||||
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
|
||||
}
|
||||
|
||||
required init(dictionary: [String:Any]) throws {
|
||||
preconditionFailure("Use init(thread:kind:) instead.")
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
internal required init?(coder: NSCoder) {
|
||||
guard let thread = coder.decodeObject(forKey: "thread") as? TSThread,
|
||||
let timestamp = coder.decodeObject(forKey: "timestamp") as? UInt64,
|
||||
let groupPublicKey = coder.decodeObject(forKey: "groupPublicKey") as? Data,
|
||||
let rawKind = coder.decodeObject(forKey: "kind") as? String else { return nil }
|
||||
switch rawKind {
|
||||
case "new":
|
||||
guard let name = coder.decodeObject(forKey: "name") as? String,
|
||||
let groupPrivateKey = coder.decodeObject(forKey: "groupPrivateKey") as? Data,
|
||||
let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
|
||||
let members = coder.decodeObject(forKey: "members") as? [Data],
|
||||
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
|
||||
self.kind = .new(groupPublicKey: groupPublicKey, name: name, groupPrivateKey: groupPrivateKey, senderKeys: senderKeys, members: members, admins: admins)
|
||||
case "info":
|
||||
guard let name = coder.decodeObject(forKey: "name") as? String,
|
||||
let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
|
||||
let members = coder.decodeObject(forKey: "members") as? [Data],
|
||||
let admins = coder.decodeObject(forKey: "admins") as? [Data] else { return nil }
|
||||
self.kind = .info(groupPublicKey: groupPublicKey, name: name, senderKeys: senderKeys, members: members, admins: admins)
|
||||
case "senderKeyRequest":
|
||||
self.kind = .senderKeyRequest(groupPublicKey: groupPublicKey)
|
||||
case "senderKey":
|
||||
guard let senderKeys = coder.decodeObject(forKey: "senderKeys") as? [ClosedGroupSenderKey],
|
||||
let senderKey = senderKeys.first else { return nil }
|
||||
self.kind = .senderKey(groupPublicKey: groupPublicKey, senderKey: senderKey)
|
||||
default: return nil
|
||||
}
|
||||
super.init(outgoingMessageWithTimestamp: timestamp, in: thread, messageBody: "",
|
||||
attachmentIds: NSMutableArray(), expiresInSeconds: 0, expireStartedAt: 0, isVoiceMessage: false,
|
||||
groupMetaMessage: .unspecified, quotedMessage: nil, contactShare: nil, linkPreview: nil)
|
||||
}
|
||||
|
||||
internal override func encode(with coder: NSCoder) {
|
||||
coder.encode(thread, forKey: "thread")
|
||||
coder.encode(timestamp, forKey: "timestamp")
|
||||
switch kind {
|
||||
case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins):
|
||||
coder.encode("new", forKey: "kind")
|
||||
coder.encode(groupPublicKey, forKey: "groupPublicKey")
|
||||
coder.encode(name, forKey: "name")
|
||||
coder.encode(groupPrivateKey, forKey: "groupPrivateKey")
|
||||
coder.encode(senderKeys, forKey: "senderKeys")
|
||||
coder.encode(members, forKey: "members")
|
||||
coder.encode(admins, forKey: "admins")
|
||||
case .info(let groupPublicKey, let name, let senderKeys, let members, let admins):
|
||||
coder.encode("info", forKey: "kind")
|
||||
coder.encode(groupPublicKey, forKey: "groupPublicKey")
|
||||
coder.encode(name, forKey: "name")
|
||||
coder.encode(senderKeys, forKey: "senderKeys")
|
||||
coder.encode(members, forKey: "members")
|
||||
coder.encode(admins, forKey: "admins")
|
||||
case .senderKeyRequest(let groupPublicKey):
|
||||
coder.encode(groupPublicKey, forKey: "groupPublicKey")
|
||||
case .senderKey(let groupPublicKey, let senderKey):
|
||||
coder.encode("senderKey", forKey: "kind")
|
||||
coder.encode(groupPublicKey, forKey: "groupPublicKey")
|
||||
coder.encode([ senderKey ], forKey: "senderKeys")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Building
|
||||
@objc internal override func dataMessageBuilder() -> Any? {
|
||||
guard let builder = super.dataMessageBuilder() as? SSKProtoDataMessage.SSKProtoDataMessageBuilder else { return nil }
|
||||
do {
|
||||
let closedGroupUpdate: SSKProtoDataMessageClosedGroupUpdate.SSKProtoDataMessageClosedGroupUpdateBuilder
|
||||
switch kind {
|
||||
case .new(let groupPublicKey, let name, let groupPrivateKey, let senderKeys, let members, let admins):
|
||||
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .new)
|
||||
closedGroupUpdate.setName(name)
|
||||
closedGroupUpdate.setGroupPrivateKey(groupPrivateKey)
|
||||
closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() })
|
||||
closedGroupUpdate.setMembers(members)
|
||||
closedGroupUpdate.setAdmins(admins)
|
||||
case .info(let groupPublicKey, let name, let senderKeys, let members, let admins):
|
||||
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .info)
|
||||
closedGroupUpdate.setName(name)
|
||||
closedGroupUpdate.setSenderKeys(try senderKeys.map { try $0.toProto() })
|
||||
closedGroupUpdate.setMembers(members)
|
||||
closedGroupUpdate.setAdmins(admins)
|
||||
case .senderKeyRequest(let groupPublicKey):
|
||||
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKeyRequest)
|
||||
case .senderKey(let groupPublicKey, let senderKey):
|
||||
closedGroupUpdate = SSKProtoDataMessageClosedGroupUpdate.builder(groupPublicKey: groupPublicKey, type: .senderKey)
|
||||
closedGroupUpdate.setSenderKeys([ try senderKey.toProto() ])
|
||||
}
|
||||
builder.setClosedGroupUpdate(try closedGroupUpdate.build())
|
||||
} catch {
|
||||
owsFailDebug("Failed to build closed group update due to error: \(error).")
|
||||
return nil
|
||||
}
|
||||
return builder
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
import CryptoSwift
|
||||
import SessionMetadataKit
|
||||
|
||||
@objc(LKClosedGroupUtilities)
|
||||
public final class ClosedGroupUtilities : NSObject {
|
||||
|
||||
@objc(LKSSKDecryptionError)
|
||||
public class SSKDecryptionError : NSError { // Not called `Error` for Obj-C interoperablity
|
||||
|
||||
@objc public static let invalidGroupPublicKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 1, userInfo: [ NSLocalizedDescriptionKey : "Invalid group public key." ])
|
||||
@objc public static let noData = SSKDecryptionError(domain: "SSKErrorDomain", code: 2, userInfo: [ NSLocalizedDescriptionKey : "Received an empty envelope." ])
|
||||
@objc public static let noGroupPrivateKey = SSKDecryptionError(domain: "SSKErrorDomain", code: 3, userInfo: [ NSLocalizedDescriptionKey : "Missing group private key." ])
|
||||
@objc public static let selfSend = SSKDecryptionError(domain: "SSKErrorDomain", code: 4, userInfo: [ NSLocalizedDescriptionKey : "Message addressed at self." ])
|
||||
}
|
||||
|
||||
@objc(encryptData:usingGroupPublicKey:transaction:error:)
|
||||
public static func encrypt(data: Data, groupPublicKey: String, transaction: YapDatabaseReadWriteTransaction) throws -> Data {
|
||||
// 1. ) Encrypt the data with the user's sender key
|
||||
guard let userPublicKey = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else {
|
||||
throw SMKError.assertionError(description: "[Loki] Couldn't find user key pair.")
|
||||
}
|
||||
let ciphertextAndKeyIndex = try SharedSenderKeysImplementation.shared.encrypt(data, forGroupWithPublicKey: groupPublicKey,
|
||||
senderPublicKey: userPublicKey, protocolContext: transaction)
|
||||
let ivAndCiphertext = ciphertextAndKeyIndex[0] as! Data
|
||||
let keyIndex = ciphertextAndKeyIndex[1] as! UInt
|
||||
let encryptedMessage = ClosedGroupCiphertextMessage(_throws_withIVAndCiphertext: ivAndCiphertext, senderPublicKey: Data(hex: userPublicKey), keyIndex: UInt32(keyIndex))
|
||||
// 2. ) Encrypt the result for the group's public key to hide the sender public key and key index
|
||||
let (ciphertext, _, ephemeralPublicKey) = try EncryptionUtilities.encrypt(encryptedMessage.serialized, using: groupPublicKey.removing05PrefixIfNeeded())
|
||||
// 3. ) Wrap the result
|
||||
return try SSKProtoClosedGroupCiphertextMessageWrapper.builder(ciphertext: ciphertext, ephemeralPublicKey: ephemeralPublicKey).build().serializedData()
|
||||
}
|
||||
|
||||
@objc(decryptEnvelope:transaction:error:)
|
||||
public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> [Any] {
|
||||
let (plaintext, senderPublicKey) = try decrypt(envelope: envelope, transaction: transaction)
|
||||
return [ plaintext, senderPublicKey ]
|
||||
}
|
||||
|
||||
public static func decrypt(envelope: SSKProtoEnvelope, transaction: YapDatabaseReadWriteTransaction) throws -> (plaintext: Data, senderPublicKey: String) {
|
||||
// 1. ) Check preconditions
|
||||
guard let groupPublicKey = envelope.source, SharedSenderKeysImplementation.shared.isClosedGroup(groupPublicKey) else {
|
||||
throw SSKDecryptionError.invalidGroupPublicKey
|
||||
}
|
||||
guard let data = envelope.content else {
|
||||
throw SSKDecryptionError.noData
|
||||
}
|
||||
guard let hexEncodedGroupPrivateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey) else {
|
||||
throw SSKDecryptionError.noGroupPrivateKey
|
||||
}
|
||||
let groupPrivateKey = Data(hex: hexEncodedGroupPrivateKey)
|
||||
// 2. ) Parse the wrapper
|
||||
let wrapper = try SSKProtoClosedGroupCiphertextMessageWrapper.parseData(data)
|
||||
let ivAndCiphertext = wrapper.ciphertext
|
||||
let ephemeralPublicKey = wrapper.ephemeralPublicKey
|
||||
// 3. ) Decrypt the data inside
|
||||
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: ephemeralPublicKey, privateKey: groupPrivateKey)
|
||||
let salt = "LOKI"
|
||||
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
||||
let closedGroupCiphertextMessageAsData = try DecryptionUtilities.decrypt(ivAndCiphertext, usingAESGCMWithSymmetricKey: Data(symmetricKey))
|
||||
// 4. ) Parse the closed group ciphertext message
|
||||
let closedGroupCiphertextMessage = ClosedGroupCiphertextMessage(_throws_with: closedGroupCiphertextMessageAsData)
|
||||
let senderPublicKey = closedGroupCiphertextMessage.senderPublicKey.toHexString()
|
||||
guard senderPublicKey != getUserHexEncodedPublicKey() else { throw SSKDecryptionError.selfSend }
|
||||
// 5. ) Use the info inside the closed group ciphertext message to decrypt the actual message content
|
||||
let plaintext = try SharedSenderKeysImplementation.shared.decrypt(closedGroupCiphertextMessage.ivAndCiphertext, forGroupWithPublicKey: groupPublicKey,
|
||||
senderPublicKey: senderPublicKey, keyIndex: UInt(closedGroupCiphertextMessage.keyIndex), protocolContext: transaction)
|
||||
// 6. ) Return
|
||||
return (plaintext, senderPublicKey)
|
||||
}
|
||||
}
|
|
@ -1,220 +0,0 @@
|
|||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionMetadataKit
|
||||
|
||||
@objc(LKSharedSenderKeysImplementation)
|
||||
public final class SharedSenderKeysImplementation : NSObject {
|
||||
private static let gcmTagSize: UInt = 16
|
||||
private static let ivSize: UInt = 12
|
||||
|
||||
// MARK: Documentation
|
||||
// A quick overview of how shared sender key based closed groups work:
|
||||
//
|
||||
// • When a user creates a group, they generate a key pair for the group along with a ratchet for
|
||||
// every member of the group. They bundle this together with some other group info such as the group
|
||||
// name in a `ClosedGroupUpdateMessage` and send that using established channels to every member of
|
||||
// the group. Note that because a user can only pick from their existing contacts when selecting
|
||||
// the group members they shouldn't need to establish sessions before being able to send the
|
||||
// `ClosedGroupUpdateMessage`. Another way to optimize the performance of the group creation process
|
||||
// is to batch fetch the device links of all members involved ahead of time, rather than letting
|
||||
// the sending pipeline do it separately for every user the `ClosedGroupUpdateMessage` is sent to.
|
||||
// • After the group is created, every user polls for the public key associated with the group.
|
||||
// • Upon receiving a `ClosedGroupUpdateMessage` of type `.new`, a user sends session requests to all
|
||||
// other members of the group they don't yet have a session with for reasons outlined below.
|
||||
// • When a user sends a message they step their ratchet and use the resulting message key to encrypt
|
||||
// the message.
|
||||
// • When another user receives that message, they step the ratchet associated with the sender and
|
||||
// use the resulting message key to decrypt the message.
|
||||
// • When a user leaves or is kicked from a group, all members must generate new ratchets to ensure that
|
||||
// removed users can't decrypt messages going forward. To this end every user deletes all ratchets
|
||||
// associated with the group in question upon receiving a group update message that indicates that
|
||||
// a user left. They then generate a new ratchet for themselves and send it out to all members of
|
||||
// the group (again fetching device links ahead of time). The user should already have established
|
||||
// sessions with all other members at this point because of the behavior outlined a few points above.
|
||||
// • When a user adds a new member to the group, they generate a ratchet for that new member and
|
||||
// send that bundled in a `ClosedGroupUpdateMessage` to the group. They send a
|
||||
// `ClosedGroupUpdateMessage` with the newly generated ratchet but also the existing ratchets of
|
||||
// every other member of the group to the user that joined.
|
||||
|
||||
// MARK: Ratcheting Error
|
||||
public enum RatchetingError : LocalizedError {
|
||||
case loadingFailed(groupPublicKey: String, senderPublicKey: String)
|
||||
case messageKeyMissing(targetKeyIndex: UInt, groupPublicKey: String, senderPublicKey: String)
|
||||
case generic
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .loadingFailed(let groupPublicKey, let senderPublicKey): return "Couldn't get ratchet for closed group with public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
|
||||
case .messageKeyMissing(let targetKeyIndex, let groupPublicKey, let senderPublicKey): return "Couldn't find message key for old key index: \(targetKeyIndex), public key: \(groupPublicKey), sender public key: \(senderPublicKey)."
|
||||
case .generic: return "An error occurred"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public static let shared = SharedSenderKeysImplementation()
|
||||
|
||||
private override init() { }
|
||||
|
||||
// MARK: Private/Internal API
|
||||
internal func generateRatchet(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) -> ClosedGroupRatchet {
|
||||
let rootChainKey = Data.getSecureRandomData(ofSize: 32)!.toHexString()
|
||||
let ratchet = ClosedGroupRatchet(chainKey: rootChainKey, keyIndex: 0, messageKeys: [])
|
||||
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: ratchet, using: transaction)
|
||||
return ratchet
|
||||
}
|
||||
|
||||
private func step(_ ratchet: ClosedGroupRatchet) throws -> ClosedGroupRatchet {
|
||||
let nextMessageKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(1) ])
|
||||
let nextChainKey = try HMAC(key: Data(hex: ratchet.chainKey).bytes, variant: .sha256).authenticate([ UInt8(2) ])
|
||||
let nextKeyIndex = ratchet.keyIndex + 1
|
||||
let messageKeys = ratchet.messageKeys + [ nextMessageKey.toHexString() ]
|
||||
return ClosedGroupRatchet(chainKey: nextChainKey.toHexString(), keyIndex: nextKeyIndex, messageKeys: messageKeys)
|
||||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
private func stepRatchetOnce(for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> ClosedGroupRatchet {
|
||||
#if DEBUG
|
||||
assert(!Thread.isMainThread)
|
||||
#endif
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
do {
|
||||
let result = try step(ratchet)
|
||||
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, using: transaction)
|
||||
return result
|
||||
} catch {
|
||||
print("[Loki] Couldn't step ratchet due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// - Note: Sync. Don't call from the main thread.
|
||||
private func stepRatchet(for groupPublicKey: String, senderPublicKey: String, until targetKeyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> ClosedGroupRatchet {
|
||||
#if DEBUG
|
||||
assert(!Thread.isMainThread)
|
||||
#endif
|
||||
let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
|
||||
guard let ratchet = Storage.getClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, from: collection) else {
|
||||
let error = RatchetingError.loadingFailed(groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
if targetKeyIndex < ratchet.keyIndex {
|
||||
// There's no need to advance the ratchet if this is invoked for an old key index
|
||||
guard ratchet.messageKeys.count > targetKeyIndex else {
|
||||
let error = RatchetingError.messageKeyMissing(targetKeyIndex: targetKeyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
print("[Loki] \(error.errorDescription!)")
|
||||
throw error
|
||||
}
|
||||
return ratchet
|
||||
} else {
|
||||
var currentKeyIndex = ratchet.keyIndex
|
||||
var result = ratchet
|
||||
while currentKeyIndex < targetKeyIndex {
|
||||
do {
|
||||
result = try step(result)
|
||||
currentKeyIndex = result.keyIndex
|
||||
} catch {
|
||||
print("[Loki] Couldn't step ratchet due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
let collection: Storage.ClosedGroupRatchetCollectionType = (isRetry) ? .old : .current
|
||||
Storage.setClosedGroupRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, ratchet: result, in: collection, using: transaction)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
@objc(encrypt:forGroupWithPublicKey:senderPublicKey:protocolContext:error:)
|
||||
public func encrypt(_ plaintext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, protocolContext: Any) throws -> [Any] {
|
||||
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
|
||||
let (ivAndCiphertext, keyIndex) = try encrypt(plaintext, for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
|
||||
return [ ivAndCiphertext, NSNumber(value: keyIndex) ]
|
||||
}
|
||||
|
||||
public func encrypt(_ plaintext: Data, for groupPublicKey: String, senderPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) throws -> (ivAndCiphertext: Data, keyIndex: UInt) {
|
||||
let ratchet: ClosedGroupRatchet
|
||||
do {
|
||||
ratchet = try stepRatchetOnce(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
|
||||
} catch {
|
||||
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
|
||||
// convenient because there's an easy way to get the sender public key from here.
|
||||
if case RatchetingError.loadingFailed(_, _) = error {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
let iv = Data.getSecureRandomData(ofSize: SharedSenderKeysImplementation.ivSize)!
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
|
||||
let messageKey = ratchet.messageKeys.last!
|
||||
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
|
||||
let ciphertext = try aes.encrypt(plaintext.bytes)
|
||||
return (ivAndCiphertext: iv + Data(bytes: ciphertext), ratchet.keyIndex)
|
||||
}
|
||||
|
||||
@objc(decrypt:forGroupWithPublicKey:senderPublicKey:keyIndex:protocolContext:error:)
|
||||
public func decrypt(_ ivAndCiphertext: Data, forGroupWithPublicKey groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, protocolContext: Any) throws -> Data {
|
||||
let transaction = protocolContext as! YapDatabaseReadWriteTransaction
|
||||
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction)
|
||||
}
|
||||
|
||||
public func decrypt(_ ivAndCiphertext: Data, for groupPublicKey: String, senderPublicKey: String, keyIndex: UInt, using transaction: YapDatabaseReadWriteTransaction, isRetry: Bool = false) throws -> Data {
|
||||
let ratchet: ClosedGroupRatchet
|
||||
do {
|
||||
ratchet = try stepRatchet(for: groupPublicKey, senderPublicKey: senderPublicKey, until: keyIndex, using: transaction, isRetry: isRetry)
|
||||
} catch {
|
||||
if !isRetry {
|
||||
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
|
||||
} else {
|
||||
// FIXME: It'd be cleaner to handle this in OWSMessageDecrypter (where all the other decryption errors are handled), but this was a lot more
|
||||
// convenient because there's an easy way to get the sender public key from here.
|
||||
if case RatchetingError.loadingFailed(_, _) = error {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
let iv = ivAndCiphertext[0..<Int(SharedSenderKeysImplementation.ivSize)]
|
||||
let ciphertext = ivAndCiphertext[Int(SharedSenderKeysImplementation.ivSize)...]
|
||||
let gcm = GCM(iv: iv.bytes, tagLength: Int(SharedSenderKeysImplementation.gcmTagSize), mode: .combined)
|
||||
let messageKeys = ratchet.messageKeys
|
||||
let lastNMessageKeys: [String]
|
||||
if messageKeys.count > 16 { // Pick an arbitrary number of message keys to try; this helps resolve issues caused by messages arriving out of order
|
||||
lastNMessageKeys = [String](messageKeys[messageKeys.index(messageKeys.endIndex, offsetBy: -16)..<messageKeys.endIndex])
|
||||
} else {
|
||||
lastNMessageKeys = messageKeys
|
||||
}
|
||||
guard !lastNMessageKeys.isEmpty else {
|
||||
throw RatchetingError.messageKeyMissing(targetKeyIndex: keyIndex, groupPublicKey: groupPublicKey, senderPublicKey: senderPublicKey)
|
||||
}
|
||||
var error: Error?
|
||||
for messageKey in lastNMessageKeys.reversed() { // Reversed because most likely the last one is the one we need
|
||||
let aes = try AES(key: Data(hex: messageKey).bytes, blockMode: gcm, padding: .noPadding)
|
||||
do {
|
||||
return Data(try aes.decrypt(ciphertext.bytes))
|
||||
} catch (let e) {
|
||||
error = e
|
||||
}
|
||||
}
|
||||
if !isRetry {
|
||||
return try decrypt(ivAndCiphertext, for: groupPublicKey, senderPublicKey: senderPublicKey, keyIndex: keyIndex, using: transaction, isRetry: true)
|
||||
} else {
|
||||
ClosedGroupsProtocol.requestSenderKey(for: groupPublicKey, senderPublicKey: senderPublicKey, using: transaction)
|
||||
throw error ?? RatchetingError.generic
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func isClosedGroup(_ publicKey: String) -> Bool {
|
||||
return Storage.getUserClosedGroupPublicKeys().contains(publicKey)
|
||||
}
|
||||
|
||||
public func getKeyPair(forGroupWithPublicKey groupPublicKey: String) -> ECKeyPair {
|
||||
let privateKey = Storage.getClosedGroupPrivateKey(for: groupPublicKey)!
|
||||
return ECKeyPair(publicKey: Data(hex: groupPublicKey.removing05PrefixIfNeeded()), privateKey: Data(hex: privateKey))!
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue