Create SessionSnodeKit
This commit is contained in:
parent
a46630c192
commit
2b1e322832
15
Podfile
15
Podfile
|
@ -105,9 +105,16 @@ target 'SignalMessaging' do
|
||||||
shared_pods
|
shared_pods
|
||||||
end
|
end
|
||||||
|
|
||||||
|
target 'SessionSnodeKit' do
|
||||||
|
pod 'CryptoSwift', :inhibit_warnings => true
|
||||||
|
pod 'Curve25519Kit', :inhibit_warnings => true
|
||||||
|
pod 'PromiseKit', :inhibit_warnings => true
|
||||||
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
enable_whole_module_optimization_for_cryptoswift(installer)
|
enable_whole_module_optimization_for_cryptoswift(installer)
|
||||||
enable_extension_support_for_purelayout(installer)
|
enable_extension_support_for_purelayout(installer)
|
||||||
|
set_minimum_deployment_target(installer)
|
||||||
end
|
end
|
||||||
|
|
||||||
def enable_whole_module_optimization_for_cryptoswift(installer)
|
def enable_whole_module_optimization_for_cryptoswift(installer)
|
||||||
|
@ -133,3 +140,11 @@ def enable_extension_support_for_purelayout(installer)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_minimum_deployment_target(installer)
|
||||||
|
installer.pods_project.targets.each do |target|
|
||||||
|
target.build_configurations.each do |build_configuration|
|
||||||
|
build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@ PODS:
|
||||||
- CocoaLumberjack/Core (= 3.6.2)
|
- CocoaLumberjack/Core (= 3.6.2)
|
||||||
- CocoaLumberjack/Core (3.6.2)
|
- CocoaLumberjack/Core (3.6.2)
|
||||||
- CryptoSwift (1.3.2)
|
- CryptoSwift (1.3.2)
|
||||||
|
- Curve25519Kit (2.1.0)
|
||||||
- FeedKit (8.1.1)
|
- FeedKit (8.1.1)
|
||||||
- GRKOpenSSLFramework (1.0.2.12)
|
- GRKOpenSSLFramework (1.0.2.12)
|
||||||
- libPhoneNumber-iOS (0.9.15)
|
- libPhoneNumber-iOS (0.9.15)
|
||||||
|
@ -199,11 +200,14 @@ PODS:
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AFNetworking (~> 3.2.1)
|
- AFNetworking (~> 3.2.1)
|
||||||
|
- CryptoSwift
|
||||||
- CryptoSwift (~> 1.3)
|
- CryptoSwift (~> 1.3)
|
||||||
|
- Curve25519Kit
|
||||||
- FeedKit (~> 8.1)
|
- FeedKit (~> 8.1)
|
||||||
- GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`)
|
- GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`)
|
||||||
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
|
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
|
||||||
- NVActivityIndicatorView (~> 4.7)
|
- NVActivityIndicatorView (~> 4.7)
|
||||||
|
- PromiseKit
|
||||||
- PromiseKit (= 6.5.3)
|
- PromiseKit (= 6.5.3)
|
||||||
- PureLayout (~> 3.1.4)
|
- PureLayout (~> 3.1.4)
|
||||||
- Reachability
|
- Reachability
|
||||||
|
@ -232,6 +236,7 @@ SPEC REPOS:
|
||||||
- AFNetworking
|
- AFNetworking
|
||||||
- CocoaLumberjack
|
- CocoaLumberjack
|
||||||
- CryptoSwift
|
- CryptoSwift
|
||||||
|
- Curve25519Kit
|
||||||
- FeedKit
|
- FeedKit
|
||||||
- libPhoneNumber-iOS
|
- libPhoneNumber-iOS
|
||||||
- NVActivityIndicatorView
|
- NVActivityIndicatorView
|
||||||
|
@ -309,6 +314,7 @@ SPEC CHECKSUMS:
|
||||||
AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057
|
AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057
|
||||||
CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94
|
CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94
|
||||||
CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060
|
CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060
|
||||||
|
Curve25519Kit: 76d0859ecb34704f7732847812363f83b23a6a59
|
||||||
FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9
|
FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9
|
||||||
GRKOpenSSLFramework: 8a3735ad41e7dc1daff460467bccd32ca5d6ae3e
|
GRKOpenSSLFramework: 8a3735ad41e7dc1daff460467bccd32ca5d6ae3e
|
||||||
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
|
||||||
|
@ -333,6 +339,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
|
||||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||||
|
|
||||||
PODFILE CHECKSUM: 8b58cc2d282fa528aa81b128b643596a9059c270
|
PODFILE CHECKSUM: a046210e9d91429c33c0fa60f7ff0362c6994f5d
|
||||||
|
|
||||||
COCOAPODS: 1.10.0.rc.1
|
COCOAPODS: 1.10.0.rc.1
|
||||||
|
|
2
Pods
2
Pods
|
@ -1 +1 @@
|
||||||
Subproject commit d1b2c2c2fe1b47ab1314192e9320f6cbc30871be
|
Subproject commit 5ba5f8d5a001bbf4d925ef0f246bf402e03b097d
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
public struct Configuration {
|
||||||
|
public let storage: Storage
|
||||||
|
|
||||||
|
internal static var shared: Configuration!
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum SessionSnodeKit { // Just to make the external API nice
|
||||||
|
|
||||||
|
public static func configure(with configuration: Configuration) {
|
||||||
|
Configuration.shared = configuration
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import Foundation
|
||||||
|
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 {
|
||||||
|
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
|
||||||
|
} else {
|
||||||
|
SNLog("\(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 {
|
||||||
|
SNLog("\(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"
|
||||||
|
SNLog("\(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 {
|
||||||
|
SNLog("Couldn't parse JSON returned by \(verb.rawValue) request to \(url).")
|
||||||
|
return seal.reject(Error.invalidJSON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
public struct Message {
|
||||||
|
/// 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
|
||||||
|
/// When the proof of work was calculated.
|
||||||
|
///
|
||||||
|
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
|
||||||
|
let timestamp: UInt64? = nil
|
||||||
|
/// The base 64 encoded proof of work.
|
||||||
|
let nonce: String? = nil
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -0,0 +1,4 @@
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
FOUNDATION_EXPORT double SessionSnodeKitVersionNumber;
|
||||||
|
FOUNDATION_EXPORT const unsigned char SessionSnodeKitVersionString[];
|
|
@ -0,0 +1,8 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension Notification.Name {
|
||||||
|
|
||||||
|
static let buildingPaths = Notification.Name("buildingPaths")
|
||||||
|
static let pathsBuilt = Notification.Name("pathsBuilt")
|
||||||
|
static let onionRequestPathCountriesLoaded = Notification.Name("onionRequestPathCountriesLoaded")
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import CryptoSwift
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
internal extension OnionRequestAPI {
|
||||||
|
|
||||||
|
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.
|
||||||
|
static func encrypt(_ payload: JSON, for destination: Destination) -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
let (promise, seal) = Promise<AESGCM.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):
|
||||||
|
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
|
||||||
|
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||||
|
let plaintext = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ])
|
||||||
|
let result = try AESGCM.encrypt(plaintext, for: snodeX25519PublicKey)
|
||||||
|
seal.fulfill(result)
|
||||||
|
case .server(_, let serverX25519PublicKey):
|
||||||
|
let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ])
|
||||||
|
let result = try AESGCM.encrypt(plaintext, for: 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.
|
||||||
|
static func encryptHop(from lhs: Destination, to rhs: Destination, using previousEncryptionResult: AESGCM.EncryptionResult) -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
var parameters: JSON
|
||||||
|
switch rhs {
|
||||||
|
case .snode(let snode):
|
||||||
|
let snodeED25519PublicKey = snode.publicKeySet.ed25519Key
|
||||||
|
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):
|
||||||
|
let snodeX25519PublicKey = snode.publicKeySet.x25519Key
|
||||||
|
x25519PublicKey = snodeX25519PublicKey
|
||||||
|
case .server(_, let serverX25519PublicKey):
|
||||||
|
x25519PublicKey = serverX25519PublicKey
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
|
||||||
|
let result = try AESGCM.encrypt(plaintext, for: x25519PublicKey)
|
||||||
|
seal.fulfill(result)
|
||||||
|
} catch (let error) {
|
||||||
|
seal.reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,427 @@
|
||||||
|
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> = []
|
||||||
|
// TODO: Just get/set paths from/in the database directly?
|
||||||
|
public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user
|
||||||
|
|
||||||
|
// MARK: Settings
|
||||||
|
public static let maxFileSize = 10_000_000 // 10 MB
|
||||||
|
/// 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: AESGCM.EncryptionResult, destinationSymmetricKey: Data)
|
||||||
|
|
||||||
|
// MARK: Private API
|
||||||
|
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
|
||||||
|
private static func testSnode(_ snode: Snode) -> Promise<Void> {
|
||||||
|
let (promise, seal) = Promise<Void>.pending()
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
let url = "\(snode.address):\(snode.port)/get_stats/v1"
|
||||||
|
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
|
||||||
|
HTTP.execute(.get, url, timeout: timeout).done2 { json in
|
||||||
|
guard let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
|
||||||
|
if version >= "2.0.7" {
|
||||||
|
seal.fulfill(())
|
||||||
|
} else {
|
||||||
|
SNLog("Unsupported snode version: \(version).")
|
||||||
|
seal.reject(Error.unsupportedSnodeVersion(version))
|
||||||
|
}
|
||||||
|
}.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 {
|
||||||
|
SNLog("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
|
||||||
|
SNLog("Testing guard snode: \(candidate).")
|
||||||
|
// Loop until a reliable guard snode is found
|
||||||
|
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
|
||||||
|
withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in getGuardSnode() }
|
||||||
|
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.
|
||||||
|
@discardableResult
|
||||||
|
private static func buildPaths(reusing reusablePaths: [Path]) -> Promise<[Path]> {
|
||||||
|
SNLog("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
|
||||||
|
}
|
||||||
|
SNLog("Built new onion request path: \(result.prettifiedDescription).")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}.map2 { paths in
|
||||||
|
OnionRequestAPI.paths = paths + reusablePaths
|
||||||
|
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
Configuration.shared.storage.setOnionRequestPaths(to: 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 = Configuration.shared.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) // 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) // 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
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
Configuration.shared.storage.setOnionRequestPaths(to: 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
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
if !paths.isEmpty {
|
||||||
|
SNLog("Persisting onion request paths to database.")
|
||||||
|
Configuration.shared.storage.setOnionRequestPaths(to: paths, using: transaction)
|
||||||
|
} else {
|
||||||
|
SNLog("Clearing onion request paths.")
|
||||||
|
Configuration.shared.storage.setOnionRequestPaths(to: [], 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: AESGCM.EncryptionResult!
|
||||||
|
var snodeToExclude: Snode?
|
||||||
|
if case .snode(let snode) = destination { snodeToExclude = snode }
|
||||||
|
return getPath(excluding: snodeToExclude).then2 { path -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
guardSnode = path.first!
|
||||||
|
// Encrypt in reverse order, i.e. the destination first
|
||||||
|
return encrypt(payload, for: destination).then2 { r -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
targetSnodeSymmetricKey = r.symmetricKey
|
||||||
|
// Recursively encrypt the layers of the onion (again in reverse order)
|
||||||
|
encryptionResult = r
|
||||||
|
var path = path
|
||||||
|
var rhs = destination
|
||||||
|
func addLayer() -> Promise<AESGCM.EncryptionResult> {
|
||||||
|
if path.isEmpty {
|
||||||
|
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
|
||||||
|
} else {
|
||||||
|
let lhs = Destination.snode(path.removeLast())
|
||||||
|
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<AESGCM.EncryptionResult> in
|
||||||
|
encryptionResult = r
|
||||||
|
rhs = lhs
|
||||||
|
return addLayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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
|
||||||
|
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
|
||||||
|
SNLog("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!
|
||||||
|
Threading.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(maxFileSize) {
|
||||||
|
SNLog("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 { json in
|
||||||
|
guard let base64EncodedIVAndCiphertext = json["result"] as? String,
|
||||||
|
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) }
|
||||||
|
do {
|
||||||
|
let data = try AESGCM.decrypt(ivAndCiphertext, with: 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
|
||||||
|
SNLog("The user's clock is out of sync with the service node network.")
|
||||||
|
seal.reject(SnodeAPI.Error.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[message.index(message.startIndex, offsetBy: prefix.count)..<message.endIndex]
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Snode : Hashable, CustomStringConvertible {
|
||||||
|
public let address: String
|
||||||
|
public let port: UInt16
|
||||||
|
public let publicKeySet: KeySet
|
||||||
|
|
||||||
|
public var ip: String {
|
||||||
|
address.removingPrefix("https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Method
|
||||||
|
public enum Method : String {
|
||||||
|
case getSwarm = "get_snodes_for_pubkey"
|
||||||
|
case getMessages = "retrieve"
|
||||||
|
case sendMessage = "store"
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Key Set
|
||||||
|
public struct KeySet : Hashable {
|
||||||
|
public let ed25519Key: String
|
||||||
|
public let x25519Key: String
|
||||||
|
|
||||||
|
public static func == (lhs: KeySet, rhs: KeySet) -> Bool {
|
||||||
|
return lhs.ed25519Key == rhs.ed25519Key && lhs.x25519Key == rhs.x25519Key
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(ed25519Key)
|
||||||
|
hasher.combine(x25519Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Initialization
|
||||||
|
internal init(address: String, port: UInt16, publicKeySet: KeySet) {
|
||||||
|
self.address = address
|
||||||
|
self.port = port
|
||||||
|
self.publicKeySet = publicKeySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Equality
|
||||||
|
public static func == (lhs: Snode, rhs: Snode) -> Bool {
|
||||||
|
return lhs.address == rhs.address && lhs.port == rhs.port && lhs.publicKeySet == rhs.publicKeySet
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Hashing
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(address)
|
||||||
|
hasher.combine(port)
|
||||||
|
publicKeySet.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Description
|
||||||
|
public var description: String { "\(address):\(port)" }
|
||||||
|
}
|
|
@ -0,0 +1,316 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
public enum SnodeAPI {
|
||||||
|
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
internal static var snodeFailureCount: [Snode:UInt] = [:]
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
internal static var snodePool: Set<Snode> = [] // TODO: Just get/set the database values directly?
|
||||||
|
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||||
|
internal static var swarmCache: [String:Set<Snode>] = [:] // TODO: Just get/set the database values directly?
|
||||||
|
|
||||||
|
// 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
|
||||||
|
public enum Error : LocalizedError {
|
||||||
|
case clockOutOfSync
|
||||||
|
case randomSnodePoolUpdatingFailed
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .clockOutOfSync: return "Your clock is out of sync with the service node network."
|
||||||
|
case .randomSnodePoolUpdatingFailed: return "Failed to update random service node pool."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Type Aliases
|
||||||
|
public typealias MessageListPromise = Promise<[JSON]>
|
||||||
|
public typealias RawResponse = Any
|
||||||
|
public typealias RawResponsePromise = Promise<RawResponse>
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
snodePool = Configuration.shared.storage.getSnodePool()
|
||||||
|
}
|
||||||
|
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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
SNLog("Populating snode pool using: \(target).")
|
||||||
|
let (promise, seal) = Promise<Snode>.pending()
|
||||||
|
attempt(maxRetryCount: 4, recoveringOn: Threading.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 Error.randomSnodePoolUpdatingFailed }
|
||||||
|
snodePool = Set(rawSnodes.compactMap { rawSnode in
|
||||||
|
guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int,
|
||||||
|
let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||||
|
SNLog("Failed to parse 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 Error.randomSnodePoolUpdatingFailed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.done2 { snode in
|
||||||
|
seal.fulfill(snode)
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
SNLog("Persisting snode pool to database.")
|
||||||
|
Configuration.shared.storage.setSnodePool(to: SnodeAPI.snodePool, using: transaction)
|
||||||
|
}
|
||||||
|
}.catch2 { error in
|
||||||
|
SNLog("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<Set<Snode>> {
|
||||||
|
if swarmCache[publicKey] == nil {
|
||||||
|
swarmCache[publicKey] = Configuration.shared.storage.getSwarm(for: publicKey)
|
||||||
|
}
|
||||||
|
if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minimumSwarmSnodeCount && !isForcedReload {
|
||||||
|
return Promise<Set<Snode>> { $0.fulfill(cachedSwarm) }
|
||||||
|
} else {
|
||||||
|
SNLog("Getting swarm for: \((publicKey == Configuration.shared.storage.getUserPublicKey()) ? "self" : publicKey).")
|
||||||
|
let parameters: [String:Any] = [ "pubKey" : publicKey ]
|
||||||
|
return getRandomSnode().then2 { snode in
|
||||||
|
attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) {
|
||||||
|
invoke(.getSwarm, on: snode, associatedWith: publicKey, parameters: parameters)
|
||||||
|
}
|
||||||
|
}.map2 { rawSnodes in
|
||||||
|
let swarm = parseSnodes(from: rawSnodes)
|
||||||
|
swarmCache[publicKey] = swarm
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setSwarm(to: swarm, for: publicKey, using: 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) {
|
||||||
|
var snodePool = SnodeAPI.snodePool
|
||||||
|
snodePool.remove(snode)
|
||||||
|
SnodeAPI.snodePool = snodePool
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setSnodePool(to: snodePool, using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func clearSnodePool() {
|
||||||
|
snodePool.removeAll()
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setSnodePool(to: [], using: 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
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setSwarm(to: swarm, for: publicKey, using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Receiving
|
||||||
|
public static func getMessages(for publicKey: String) -> Promise<Set<MessageListPromise>> {
|
||||||
|
let (promise, seal) = Promise<Set<MessageListPromise>>.pending()
|
||||||
|
Threading.workQueue.async {
|
||||||
|
attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
||||||
|
getTargetSnodes(for: publicKey).mapValues2 { targetSnode in
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.pruneLastMessageHashInfoIfExpired(for: targetSnode, associatedWith: publicKey, using: transaction)
|
||||||
|
}
|
||||||
|
let lastHash = Configuration.shared.storage.getLastMessageHash(for: targetSnode, associatedWith: publicKey) ?? ""
|
||||||
|
let parameters = [ "pubKey" : publicKey, "lastHash" : lastHash ]
|
||||||
|
return invoke(.getMessages, on: targetSnode, associatedWith: publicKey, parameters: parameters).map2 { rawResponse in
|
||||||
|
parseRawMessagesResponse(rawResponse, from: targetSnode, associatedWith: publicKey)
|
||||||
|
}
|
||||||
|
}.map2 { Set($0) }
|
||||||
|
}.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Sending
|
||||||
|
public static func sendMessage(_ message: Message) -> Promise<Set<RawResponsePromise>> {
|
||||||
|
let (promise, seal) = Promise<Set<RawResponsePromise>>.pending()
|
||||||
|
let publicKey = message.recipientPublicKey
|
||||||
|
Threading.workQueue.async {
|
||||||
|
getTargetSnodes(for: publicKey).map2 { targetSnodes in
|
||||||
|
let parameters = message.toJSON()
|
||||||
|
return Set(targetSnodes.map { targetSnode in
|
||||||
|
let result = attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) {
|
||||||
|
invoke(.sendMessage, on: targetSnode, associatedWith: publicKey, parameters: parameters)
|
||||||
|
}
|
||||||
|
result.done2 { rawResponse in
|
||||||
|
if let json = rawResponse as? JSON, let powDifficulty = json["difficulty"] as? Int {
|
||||||
|
guard powDifficulty != SnodeAPI.powDifficulty, powDifficulty < 100 else { return }
|
||||||
|
SNLog("Setting proof of work difficulty to \(powDifficulty).")
|
||||||
|
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||||
|
} else {
|
||||||
|
SNLog("Failed to update proof of work difficulty from: \(rawResponse).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}.done2 { seal.fulfill($0) }.catch2 { seal.reject($0) }
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) -> Set<Snode> {
|
||||||
|
guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else {
|
||||||
|
SNLog("Failed to parse targets from: \(rawResponse).")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return Set(rawSnodes.compactMap { rawSnode in
|
||||||
|
guard let address = rawSnode["ip"] as? String, let portAsString = rawSnode["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawSnode["pubkey_ed25519"] as? String, let x25519PublicKey = rawSnode["pubkey_x25519"] as? String, address != "0.0.0.0" else {
|
||||||
|
SNLog("Failed to parse 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) -> [JSON] {
|
||||||
|
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
|
||||||
|
updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages)
|
||||||
|
return removeDuplicates(from: rawMessages, associatedWith: publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) {
|
||||||
|
if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 {
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey,
|
||||||
|
to: [ "hash" : lastHash, "expirationDate" : NSNumber(value: expirationDate) ], using: transaction)
|
||||||
|
}
|
||||||
|
} else if (!rawMessages.isEmpty) {
|
||||||
|
SNLog("Failed to update last message hash value from: \(rawMessages).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeDuplicates(from rawMessages: [JSON], associatedWith publicKey: String) -> [JSON] {
|
||||||
|
var receivedMessages = Configuration.shared.storage.getReceivedMessages(for: publicKey)
|
||||||
|
return rawMessages.filter { rawMessage in
|
||||||
|
guard let hash = rawMessage["hash"] as? String else {
|
||||||
|
SNLog("Missing hash value for message: \(rawMessage).")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let isDuplicate = receivedMessages.contains(hash)
|
||||||
|
receivedMessages.insert(hash)
|
||||||
|
Configuration.shared.storage.with { transaction in
|
||||||
|
Configuration.shared.storage.setReceivedMessages(to: receivedMessages, for: publicKey, using: transaction)
|
||||||
|
}
|
||||||
|
return !isDuplicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Error Handling
|
||||||
|
/// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions.
|
||||||
|
@discardableResult
|
||||||
|
internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? {
|
||||||
|
func handleBadSnode() {
|
||||||
|
let oldFailureCount = SnodeAPI.snodeFailureCount[snode] ?? 0
|
||||||
|
let newFailureCount = oldFailureCount + 1
|
||||||
|
SnodeAPI.snodeFailureCount[snode] = newFailureCount
|
||||||
|
SNLog("Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
|
||||||
|
if newFailureCount >= SnodeAPI.snodeFailureThreshold {
|
||||||
|
SNLog("Failure threshold reached for: \(snode); dropping it.")
|
||||||
|
if let publicKey = publicKey {
|
||||||
|
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||||
|
}
|
||||||
|
SnodeAPI.dropSnodeFromSnodePool(snode)
|
||||||
|
SNLog("Snode pool count: \(snodePool.count).")
|
||||||
|
SnodeAPI.snodeFailureCount[snode] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch statusCode {
|
||||||
|
case 0, 400, 500, 503:
|
||||||
|
// The snode is unreachable
|
||||||
|
handleBadSnode()
|
||||||
|
case 406:
|
||||||
|
SNLog("The user's clock is out of sync with the service node network.")
|
||||||
|
return Error.clockOutOfSync
|
||||||
|
case 421:
|
||||||
|
// The snode isn't associated with the given public key anymore
|
||||||
|
if let publicKey = publicKey {
|
||||||
|
SNLog("Invalidating swarm for: \(publicKey).")
|
||||||
|
SnodeAPI.dropSnodeFromSwarmIfNeeded(snode, publicKey: publicKey)
|
||||||
|
} else {
|
||||||
|
SNLog("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 {
|
||||||
|
SNLog("Setting proof of work difficulty to \(powDifficulty).")
|
||||||
|
SnodeAPI.powDifficulty = UInt(powDifficulty)
|
||||||
|
} else {
|
||||||
|
handleBadSnode()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SNLog("Failed to update proof of work difficulty.")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
handleBadSnode()
|
||||||
|
SNLog("Unhandled response code: \(statusCode).")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
public protocol Storage {
|
||||||
|
|
||||||
|
func with(_ work: (Any) -> Void)
|
||||||
|
|
||||||
|
func getUserPublicKey() -> String?
|
||||||
|
func getOnionRequestPaths() -> [OnionRequestAPI.Path]
|
||||||
|
func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any)
|
||||||
|
func getSnodePool() -> Set<Snode>
|
||||||
|
func setSnodePool(to snodePool: Set<Snode>, using transaction: Any)
|
||||||
|
func getSwarm(for publicKey: String) -> Set<Snode>
|
||||||
|
func setSwarm(to swarm: Set<Snode>, for publicKey: String, using transaction: Any)
|
||||||
|
func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String?
|
||||||
|
func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any)
|
||||||
|
func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String, using transaction: Any)
|
||||||
|
func getReceivedMessages(for publicKey: String) -> Set<String>
|
||||||
|
func setReceivedMessages(to receivedMessages: Set<String>, for publicKey: String, using transaction: Any)
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import CryptoSwift
|
||||||
|
import Curve25519Kit
|
||||||
|
|
||||||
|
internal enum AESGCM {
|
||||||
|
internal static let gcmTagSize: UInt = 16
|
||||||
|
internal static let ivSize: UInt = 12
|
||||||
|
|
||||||
|
internal struct EncryptionResult { internal let ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data }
|
||||||
|
|
||||||
|
internal enum Error : LocalizedError {
|
||||||
|
case keyPairGenerationFailed
|
||||||
|
case sharedSecretGenerationFailed
|
||||||
|
|
||||||
|
public var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .keyPairGenerationFailed: return "Couldn't generate a key pair."
|
||||||
|
case .sharedSecretGenerationFailed: return "Couldn't generate a shared secret."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Note: Sync. Don't call from the main thread.
|
||||||
|
internal static func decrypt(_ ivAndCiphertext: Data, with 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(ivSize)]
|
||||||
|
let ciphertext = ivAndCiphertext[Int(ivSize)...]
|
||||||
|
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
|
||||||
|
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
|
||||||
|
return Data(try aes.decrypt(ciphertext.bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Note: Sync. Don't call from the main thread.
|
||||||
|
internal static func encrypt(_ plaintext: Data, with 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(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Note: Sync. Don't call from the main thread.
|
||||||
|
internal static func encrypt(_ plaintext: Data, for 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)
|
||||||
|
guard let ephemeralKeyPair = Curve25519.generateKeyPair() else {
|
||||||
|
throw Error.keyPairGenerationFailed
|
||||||
|
}
|
||||||
|
guard let ephemeralSharedSecret = Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, andKeyPair: ephemeralKeyPair) else {
|
||||||
|
throw Error.sharedSecretGenerationFailed
|
||||||
|
}
|
||||||
|
let salt = "LOKI"
|
||||||
|
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
|
||||||
|
let ciphertext = try encrypt(plaintext, with: Data(symmetricKey))
|
||||||
|
return EncryptionResult(ciphertext: ciphertext, symmetricKey: Data(symmetricKey), ephemeralPublicKey: ephemeralKeyPair.publicKey())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
internal extension Array where Element : CustomStringConvertible {
|
||||||
|
|
||||||
|
var prettifiedDescription: String {
|
||||||
|
return "[ " + map { $0.description }.joined(separator: ", ") + " ]"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
internal extension Data {
|
||||||
|
|
||||||
|
/// Returns `size` bytes of random data generated using the default secure random number generator. See
|
||||||
|
/// [SecRandomCopyBytes](https://developer.apple.com/documentation/security/1399291-secrandomcopybytes) for more information.
|
||||||
|
static func getSecureRandomData(ofSize size: UInt) -> Data? {
|
||||||
|
var data = Data(count: Int(size))
|
||||||
|
let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) }
|
||||||
|
guard result == errSecSuccess else { return nil }
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from inputStream: InputStream) throws {
|
||||||
|
self.init()
|
||||||
|
inputStream.open()
|
||||||
|
defer { inputStream.close() }
|
||||||
|
let bufferSize = 1024
|
||||||
|
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
||||||
|
defer { buffer.deallocate() }
|
||||||
|
while inputStream.hasBytesAvailable {
|
||||||
|
let count = inputStream.read(buffer, maxLength: bufferSize)
|
||||||
|
if count < 0 {
|
||||||
|
throw inputStream.streamError!
|
||||||
|
} else if count == 0 {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
append(buffer, count: count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
internal extension Dictionary {
|
||||||
|
|
||||||
|
var prettifiedDescription: String {
|
||||||
|
return "[ " + map { key, value in
|
||||||
|
let keyDescription = String(describing: key)
|
||||||
|
let valueDescription = String(describing: value)
|
||||||
|
let maxLength = 20
|
||||||
|
let truncatedValueDescription = valueDescription.count > maxLength ? valueDescription.prefix(maxLength) + "..." : valueDescription
|
||||||
|
return keyDescription + " : " + truncatedValueDescription
|
||||||
|
}.joined(separator: ", ") + " ]"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
public typealias JSON = [String:Any]
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
internal func SNLog(_ message: String) {
|
||||||
|
#if DEBUG
|
||||||
|
print("[Session] \(message)")
|
||||||
|
#endif
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
/// Delay the execution of the promise constructed in `body` by `delay` seconds.
|
||||||
|
internal func withDelay<T>(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
||||||
|
#if DEBUG
|
||||||
|
assert(Thread.current.isMainThread) // Timers don't do well on background queues
|
||||||
|
#endif
|
||||||
|
let (promise, seal) = Promise<T>.pending()
|
||||||
|
Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { _ in
|
||||||
|
body().done(on: completionQueue) { seal.fulfill($0) }.catch(on: completionQueue) { seal.reject($0) }
|
||||||
|
}
|
||||||
|
return promise
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
extension Promise : Hashable {
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
let reference = ObjectIdentifier(self)
|
||||||
|
hasher.combine(reference.hashValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: Promise, rhs: Promise) -> Bool {
|
||||||
|
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
/// Retry the promise constructed in `body` up to `maxRetryCount` times.
|
||||||
|
internal func attempt<T>(maxRetryCount: UInt, recoveringOn queue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
||||||
|
var retryCount = 0
|
||||||
|
func attempt() -> Promise<T> {
|
||||||
|
return body().recover(on: queue) { error -> Promise<T> in
|
||||||
|
guard retryCount < maxRetryCount else { throw error }
|
||||||
|
retryCount += 1
|
||||||
|
return attempt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attempt()
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
import PromiseKit
|
||||||
|
|
||||||
|
internal extension Thenable {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func then2<U>(_ body: @escaping (T) throws -> U) -> Promise<U.T> where U : Thenable {
|
||||||
|
return then(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func map2<U>(_ transform: @escaping (T) throws -> U) -> Promise<U> {
|
||||||
|
return map(on: Threading.workQueue, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func done2(_ body: @escaping (T) throws -> Void) -> Promise<Void> {
|
||||||
|
return done(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func get2(_ body: @escaping (T) throws -> Void) -> Promise<T> {
|
||||||
|
return get(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension Thenable where T: Sequence {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func mapValues2<U>(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> {
|
||||||
|
return mapValues(on: Threading.workQueue, transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension Guarantee {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func then2<U>(_ body: @escaping (T) -> Guarantee<U>) -> Guarantee<U> {
|
||||||
|
return then(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func map2<U>(_ body: @escaping (T) -> U) -> Guarantee<U> {
|
||||||
|
return map(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func done2(_ body: @escaping (T) -> Void) -> Guarantee<Void> {
|
||||||
|
return done(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func get2(_ body: @escaping (T) -> Void) -> Guarantee<T> {
|
||||||
|
return get(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension CatchMixin {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer {
|
||||||
|
return self.catch(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func recover2<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T {
|
||||||
|
return recover(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func recover2(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T> {
|
||||||
|
return recover(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func ensure2(_ body: @escaping () -> Void) -> Promise<T> {
|
||||||
|
return ensure(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal extension CatchMixin where T == Void {
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func recover2(_ body: @escaping(Error) -> Void) -> Guarantee<Void> {
|
||||||
|
return recover(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func recover2(_ body: @escaping(Error) throws -> Void) -> Promise<Void> {
|
||||||
|
return recover(on: Threading.workQueue, body)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
internal extension String {
|
||||||
|
|
||||||
|
func removingPrefix(_ prefix: String) -> String {
|
||||||
|
guard let range = self.range(of: prefix), range.lowerBound == startIndex else { return self }
|
||||||
|
return String(self[range.upperBound..<endIndex])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
internal enum Threading {
|
||||||
|
|
||||||
|
internal static let workQueue = DispatchQueue(label: "SessionSnodeKit.workQueue", qos: .userInitiated) // It's important that this is a serial queue
|
||||||
|
}
|
|
@ -469,6 +469,7 @@
|
||||||
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
|
||||||
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
||||||
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
|
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
|
||||||
|
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; };
|
||||||
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
|
||||||
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
|
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
|
||||||
A123C14916F902EE000AE905 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; };
|
A123C14916F902EE000AE905 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; };
|
||||||
|
@ -596,6 +597,30 @@
|
||||||
C39DD28824F3318C008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
C39DD28824F3318C008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
||||||
C39DD28A24F3336E008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
C39DD28A24F3336E008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
||||||
C39DD28B24F3336F008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
C39DD28B24F3336F008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; };
|
||||||
|
C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||||
|
C3C2A5A6255385C100C340D1 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
||||||
|
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||||
|
C3C2A5BF255385EE00C340D1 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B6255385EC00C340D1 /* Message.swift */; };
|
||||||
|
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B7255385EC00C340D1 /* Snode.swift */; };
|
||||||
|
C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B8255385EC00C340D1 /* Storage.swift */; };
|
||||||
|
C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5B9255385ED00C340D1 /* Configuration.swift */; };
|
||||||
|
C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */; };
|
||||||
|
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */; };
|
||||||
|
C3C2A5C5255385EE00C340D1 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; };
|
||||||
|
C3C2A5C6255385EE00C340D1 /* Notification+Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BD255385EE00C340D1 /* Notification+Session.swift */; };
|
||||||
|
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */; };
|
||||||
|
C3C2A5DA2553860B00C340D1 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CE2553860700C340D1 /* Logging.swift */; };
|
||||||
|
C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */; };
|
||||||
|
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D02553860800C340D1 /* Promise+Threading.swift */; };
|
||||||
|
C3C2A5DD2553860B00C340D1 /* Array+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Utilities.swift */; };
|
||||||
|
C3C2A5DE2553860B00C340D1 /* String+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Utilities.swift */; };
|
||||||
|
C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; };
|
||||||
|
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; };
|
||||||
|
C3C2A5E12553860B00C340D1 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; };
|
||||||
|
C3C2A5E22553860B00C340D1 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; };
|
||||||
|
C3C2A5E32553860B00C340D1 /* AESGCM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D72553860B00C340D1 /* AESGCM.swift */; };
|
||||||
|
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; };
|
||||||
|
C3C2A5E52553860B00C340D1 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; };
|
||||||
C3C3CF8924D8EED300E1CCE7 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
|
C3C3CF8924D8EED300E1CCE7 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C3CF8824D8EED300E1CCE7 /* TextView.swift */; };
|
||||||
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
|
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
|
||||||
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
|
||||||
|
@ -677,6 +702,13 @@
|
||||||
remoteGlobalIDString = 453518911FC63DBF00210559;
|
remoteGlobalIDString = 453518911FC63DBF00210559;
|
||||||
remoteInfo = SignalMessaging;
|
remoteInfo = SignalMessaging;
|
||||||
};
|
};
|
||||||
|
C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = C3C2A59E255385C100C340D1;
|
||||||
|
remoteInfo = SessionSnodeKit;
|
||||||
|
};
|
||||||
/* End PBXContainerItemProxy section */
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
@ -698,6 +730,7 @@
|
||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolderSpec = 10;
|
dstSubfolderSpec = 10;
|
||||||
files = (
|
files = (
|
||||||
|
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
|
||||||
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
|
4535189A1FC63DBF00210559 /* SignalMessaging.framework in Embed Frameworks */,
|
||||||
);
|
);
|
||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
|
@ -1258,6 +1291,7 @@
|
||||||
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>"; };
|
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>"; };
|
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>"; };
|
||||||
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>"; };
|
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>"; };
|
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>"; };
|
||||||
A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||||
A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
A163E8AA16F3F6A90094D68B /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||||
|
@ -1265,6 +1299,7 @@
|
||||||
A1C32D4F17A06537000A904E /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; };
|
A1C32D4F17A06537000A904E /* AddressBookUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBookUI.framework; path = System/Library/Frameworks/AddressBookUI.framework; sourceTree = SDKROOT; };
|
||||||
A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
A1FDCBEE16DAA6C300868894 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
|
||||||
A5509EC91A69AB8B00ABA4BC /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Storyboard/Main.storyboard; sourceTree = "<group>"; };
|
A5509EC91A69AB8B00ABA4BC /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = Storyboard/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
A6344D429FFAC3B44E6A06FA /* Pods-SessionSnodeKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionSnodeKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionSnodeKit/Pods-SessionSnodeKit.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
AD2AB1207E8888E4262D781B /* Pods-SignalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.debug.xcconfig"; sourceTree = "<group>"; };
|
AD2AB1207E8888E4262D781B /* Pods-SignalTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalTests/Pods-SignalTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = audio_pause_button_blue.png; sourceTree = "<group>"; };
|
AD83FF381A73426500B5C81A /* audio_pause_button_blue.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = audio_pause_button_blue.png; sourceTree = "<group>"; };
|
||||||
AD83FF391A73426500B5C81A /* audio_pause_button_blue@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "audio_pause_button_blue@2x.png"; sourceTree = "<group>"; };
|
AD83FF391A73426500B5C81A /* audio_pause_button_blue@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "audio_pause_button_blue@2x.png"; sourceTree = "<group>"; };
|
||||||
|
@ -1363,6 +1398,7 @@
|
||||||
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
|
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
|
||||||
B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = "<group>"; };
|
B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = "<group>"; };
|
||||||
B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
|
B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
|
||||||
|
C022DD8E076866C6241610BF /* Pods-SessionSnodeKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionSnodeKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionSnodeKit/Pods-SessionSnodeKit.app store release.xcconfig"; sourceTree = "<group>"; };
|
||||||
C31A6C59247F214E001123EF /* UIView+Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Glow.swift"; sourceTree = "<group>"; };
|
C31A6C59247F214E001123EF /* UIView+Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Glow.swift"; sourceTree = "<group>"; };
|
||||||
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
|
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
C31D1DD225216101005D4DA8 /* UIView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utilities.swift"; sourceTree = "<group>"; };
|
C31D1DD225216101005D4DA8 /* UIView+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1400,6 +1436,30 @@
|
||||||
C39DD28724F3318C008590FC /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
|
C39DD28724F3318C008590FC /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
|
||||||
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; 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>"; };
|
C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
|
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SessionSnodeKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SessionSnodeKit.h; sourceTree = "<group>"; };
|
||||||
|
C3C2A5A2255385C100C340D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
C3C2A5B6255385EC00C340D1 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5B7255385EC00C340D1 /* Snode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snode.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5B8255385EC00C340D1 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5B9255385ED00C340D1 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnionRequestAPI.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OnionRequestAPI+Encryption.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5BC255385EE00C340D1 /* HTTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTP.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5BD255385EE00C340D1 /* Notification+Session.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+Session.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Hashing.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D02553860800C340D1 /* Promise+Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Threading.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D22553860900C340D1 /* String+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Delaying.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D42553860A00C340D1 /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Retrying.swift"; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
||||||
C3C3CF8824D8EED300E1CCE7 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
|
C3C3CF8824D8EED300E1CCE7 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = "<group>"; };
|
||||||
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundPoller.swift; sourceTree = "<group>"; };
|
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundPoller.swift; sourceTree = "<group>"; };
|
||||||
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = "<group>"; };
|
C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SRCopyableLabel.swift; sourceTree = "<group>"; };
|
||||||
|
@ -1462,6 +1522,14 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C3C2A59C255385C100C340D1 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D221A086169C9E5E00537ABF /* Frameworks */ = {
|
D221A086169C9E5E00537ABF /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -1481,6 +1549,7 @@
|
||||||
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */,
|
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */,
|
||||||
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */,
|
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */,
|
||||||
453518991FC63DBF00210559 /* SignalMessaging.framework in Frameworks */,
|
453518991FC63DBF00210559 /* SignalMessaging.framework in Frameworks */,
|
||||||
|
C3C2A5A6255385C100C340D1 /* SessionSnodeKit.framework in Frameworks */,
|
||||||
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */,
|
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */,
|
||||||
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */,
|
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */,
|
||||||
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */,
|
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */,
|
||||||
|
@ -2497,6 +2566,8 @@
|
||||||
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */,
|
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */,
|
||||||
F62ECF7B8AF4F8089AA705B3 /* Pods-LokiPushNotificationService.debug.xcconfig */,
|
F62ECF7B8AF4F8089AA705B3 /* Pods-LokiPushNotificationService.debug.xcconfig */,
|
||||||
18D19142FD6E60FD0A5D89F7 /* Pods-LokiPushNotificationService.app store release.xcconfig */,
|
18D19142FD6E60FD0A5D89F7 /* Pods-LokiPushNotificationService.app store release.xcconfig */,
|
||||||
|
A6344D429FFAC3B44E6A06FA /* Pods-SessionSnodeKit.debug.xcconfig */,
|
||||||
|
C022DD8E076866C6241610BF /* Pods-SessionSnodeKit.app store release.xcconfig */,
|
||||||
);
|
);
|
||||||
name = Pods;
|
name = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2803,6 +2874,52 @@
|
||||||
path = SwiftCSV;
|
path = SwiftCSV;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C3C2A5B0255385C700C340D1 /* Meta */,
|
||||||
|
C3C2A5B9255385ED00C340D1 /* Configuration.swift */,
|
||||||
|
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
|
||||||
|
C3C2A5B6255385EC00C340D1 /* Message.swift */,
|
||||||
|
C3C2A5BD255385EE00C340D1 /* Notification+Session.swift */,
|
||||||
|
C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */,
|
||||||
|
C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */,
|
||||||
|
C3C2A5B7255385EC00C340D1 /* Snode.swift */,
|
||||||
|
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */,
|
||||||
|
C3C2A5B8255385EC00C340D1 /* Storage.swift */,
|
||||||
|
C3C2A5CD255385F300C340D1 /* Utilities */,
|
||||||
|
);
|
||||||
|
path = SessionSnodeKit;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C3C2A5B0255385C700C340D1 /* Meta */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C3C2A5A1255385C100C340D1 /* SessionSnodeKit.h */,
|
||||||
|
C3C2A5A2255385C100C340D1 /* Info.plist */,
|
||||||
|
);
|
||||||
|
path = Meta;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C3C2A5CD255385F300C340D1 /* Utilities */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
C3C2A5D72553860B00C340D1 /* AESGCM.swift */,
|
||||||
|
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */,
|
||||||
|
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */,
|
||||||
|
C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */,
|
||||||
|
C3C2A5D92553860B00C340D1 /* JSON.swift */,
|
||||||
|
C3C2A5CE2553860700C340D1 /* Logging.swift */,
|
||||||
|
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */,
|
||||||
|
C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */,
|
||||||
|
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */,
|
||||||
|
C3C2A5D02553860800C340D1 /* Promise+Threading.swift */,
|
||||||
|
C3C2A5D22553860900C340D1 /* String+Utilities.swift */,
|
||||||
|
C3C2A5D42553860A00C340D1 /* Threading.swift */,
|
||||||
|
);
|
||||||
|
path = Utilities;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D221A07E169C9E5E00537ABF = {
|
D221A07E169C9E5E00537ABF = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -2810,6 +2927,7 @@
|
||||||
453518691FC635DD00210559 /* SignalShareExtension */,
|
453518691FC635DD00210559 /* SignalShareExtension */,
|
||||||
453518931FC63DBF00210559 /* SignalMessaging */,
|
453518931FC63DBF00210559 /* SignalMessaging */,
|
||||||
7BC01A3C241F40AB00BC7C55 /* LokiPushNotificationService */,
|
7BC01A3C241F40AB00BC7C55 /* LokiPushNotificationService */,
|
||||||
|
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
|
||||||
D221A08C169C9E5E00537ABF /* Frameworks */,
|
D221A08C169C9E5E00537ABF /* Frameworks */,
|
||||||
D221A08A169C9E5E00537ABF /* Products */,
|
D221A08A169C9E5E00537ABF /* Products */,
|
||||||
9404664EC513585B05DF1350 /* Pods */,
|
9404664EC513585B05DF1350 /* Pods */,
|
||||||
|
@ -2824,6 +2942,7 @@
|
||||||
453518681FC635DD00210559 /* SignalShareExtension.appex */,
|
453518681FC635DD00210559 /* SignalShareExtension.appex */,
|
||||||
453518921FC63DBF00210559 /* SignalMessaging.framework */,
|
453518921FC63DBF00210559 /* SignalMessaging.framework */,
|
||||||
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
|
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
|
||||||
|
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2871,6 +2990,7 @@
|
||||||
748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */,
|
748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */,
|
||||||
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
|
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
|
||||||
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
|
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
|
||||||
|
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2988,6 +3108,14 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C3C2A59A255385C100C340D1 /* Headers */ = {
|
||||||
|
isa = PBXHeadersBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXHeadersBuildPhase section */
|
/* End PBXHeadersBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
@ -3048,6 +3176,25 @@
|
||||||
productReference = 7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */;
|
productReference = 7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */;
|
||||||
productType = "com.apple.product-type.app-extension";
|
productType = "com.apple.product-type.app-extension";
|
||||||
};
|
};
|
||||||
|
C3C2A59E255385C100C340D1 /* SessionSnodeKit */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */;
|
||||||
|
buildPhases = (
|
||||||
|
B19B891E99B1507CAC8AAD19 /* [CP] Check Pods Manifest.lock */,
|
||||||
|
C3C2A59A255385C100C340D1 /* Headers */,
|
||||||
|
C3C2A59B255385C100C340D1 /* Sources */,
|
||||||
|
C3C2A59C255385C100C340D1 /* Frameworks */,
|
||||||
|
C3C2A59D255385C100C340D1 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = SessionSnodeKit;
|
||||||
|
productName = SessionSnodeKit;
|
||||||
|
productReference = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */;
|
||||||
|
productType = "com.apple.product-type.framework";
|
||||||
|
};
|
||||||
D221A088169C9E5E00537ABF /* Signal */ = {
|
D221A088169C9E5E00537ABF /* Signal */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = D221A0BC169C9E5F00537ABF /* Build configuration list for PBXNativeTarget "Signal" */;
|
buildConfigurationList = D221A0BC169C9E5F00537ABF /* Build configuration list for PBXNativeTarget "Signal" */;
|
||||||
|
@ -3068,6 +3215,7 @@
|
||||||
453518711FC635DD00210559 /* PBXTargetDependency */,
|
453518711FC635DD00210559 /* PBXTargetDependency */,
|
||||||
453518981FC63DBF00210559 /* PBXTargetDependency */,
|
453518981FC63DBF00210559 /* PBXTargetDependency */,
|
||||||
7BC01A41241F40AB00BC7C55 /* PBXTargetDependency */,
|
7BC01A41241F40AB00BC7C55 /* PBXTargetDependency */,
|
||||||
|
C3C2A5A5255385C100C340D1 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Signal;
|
name = Signal;
|
||||||
productName = RedPhone;
|
productName = RedPhone;
|
||||||
|
@ -3139,6 +3287,12 @@
|
||||||
DevelopmentTeam = SUQ8J2PCT7;
|
DevelopmentTeam = SUQ8J2PCT7;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
|
C3C2A59E255385C100C340D1 = {
|
||||||
|
CreatedOnToolsVersion = 12.1;
|
||||||
|
DevelopmentTeam = SUQ8J2PCT7;
|
||||||
|
LastSwiftMigration = 1210;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
D221A088169C9E5E00537ABF = {
|
D221A088169C9E5E00537ABF = {
|
||||||
DevelopmentTeam = SUQ8J2PCT7;
|
DevelopmentTeam = SUQ8J2PCT7;
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1020;
|
||||||
|
@ -3208,6 +3362,7 @@
|
||||||
453518671FC635DD00210559 /* SignalShareExtension */,
|
453518671FC635DD00210559 /* SignalShareExtension */,
|
||||||
453518911FC63DBF00210559 /* SignalMessaging */,
|
453518911FC63DBF00210559 /* SignalMessaging */,
|
||||||
7BC01A3A241F40AB00BC7C55 /* LokiPushNotificationService */,
|
7BC01A3A241F40AB00BC7C55 /* LokiPushNotificationService */,
|
||||||
|
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
@ -3243,6 +3398,13 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C3C2A59D255385C100C340D1 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D221A087169C9E5E00537ABF /* Resources */ = {
|
D221A087169C9E5E00537ABF /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -3472,6 +3634,7 @@
|
||||||
"${BUILT_PRODUCTS_DIR}/YapDatabase/YapDatabase.framework",
|
"${BUILT_PRODUCTS_DIR}/YapDatabase/YapDatabase.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/ZXingObjC/ZXingObjC.framework",
|
"${BUILT_PRODUCTS_DIR}/ZXingObjC/ZXingObjC.framework",
|
||||||
"${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework",
|
"${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework",
|
||||||
|
"${BUILT_PRODUCTS_DIR}/Curve25519Kit/Curve25519Kit.framework",
|
||||||
);
|
);
|
||||||
name = "[CP] Embed Pods Frameworks";
|
name = "[CP] Embed Pods Frameworks";
|
||||||
outputPaths = (
|
outputPaths = (
|
||||||
|
@ -3501,6 +3664,7 @@
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YapDatabase.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YapDatabase.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZXingObjC.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZXingObjC.framework",
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework",
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework",
|
||||||
|
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Curve25519Kit.framework",
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
shellPath = /bin/sh;
|
shellPath = /bin/sh;
|
||||||
|
@ -3525,6 +3689,28 @@
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
showEnvVarsInLog = 0;
|
showEnvVarsInLog = 0;
|
||||||
};
|
};
|
||||||
|
B19B891E99B1507CAC8AAD19 /* [CP] Check Pods Manifest.lock */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputFileListPaths = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||||
|
"${PODS_ROOT}/Manifest.lock",
|
||||||
|
);
|
||||||
|
name = "[CP] Check Pods Manifest.lock";
|
||||||
|
outputFileListPaths = (
|
||||||
|
);
|
||||||
|
outputPaths = (
|
||||||
|
"$(DERIVED_FILE_DIR)/Pods-SessionSnodeKit-checkManifestLockResult.txt",
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||||
|
showEnvVarsInLog = 0;
|
||||||
|
};
|
||||||
B4E9B04E862FB64FC9A8F79B /* [CP] Embed Pods Frameworks */ = {
|
B4E9B04E862FB64FC9A8F79B /* [CP] Embed Pods Frameworks */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -3776,6 +3962,34 @@
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
C3C2A59B255385C100C340D1 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */,
|
||||||
|
C3C2A5BF255385EE00C340D1 /* Message.swift in Sources */,
|
||||||
|
C3C2A5DA2553860B00C340D1 /* Logging.swift in Sources */,
|
||||||
|
C3C2A5E22553860B00C340D1 /* Promise+Retrying.swift in Sources */,
|
||||||
|
C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */,
|
||||||
|
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */,
|
||||||
|
C3C2A5E12553860B00C340D1 /* Dictionary+Utilities.swift in Sources */,
|
||||||
|
C3C2A5C5255385EE00C340D1 /* HTTP.swift in Sources */,
|
||||||
|
C3C2A5C6255385EE00C340D1 /* Notification+Session.swift in Sources */,
|
||||||
|
C3C2A5DF2553860B00C340D1 /* Promise+Delaying.swift in Sources */,
|
||||||
|
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */,
|
||||||
|
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */,
|
||||||
|
C3C2A5DD2553860B00C340D1 /* Array+Utilities.swift in Sources */,
|
||||||
|
C3C2A5E32553860B00C340D1 /* AESGCM.swift in Sources */,
|
||||||
|
C3C2A5DE2553860B00C340D1 /* String+Utilities.swift in Sources */,
|
||||||
|
C3C2A5E52553860B00C340D1 /* JSON.swift in Sources */,
|
||||||
|
C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */,
|
||||||
|
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */,
|
||||||
|
C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */,
|
||||||
|
C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */,
|
||||||
|
C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
D221A085169C9E5E00537ABF /* Sources */ = {
|
D221A085169C9E5E00537ABF /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
@ -4102,6 +4316,11 @@
|
||||||
target = 453518911FC63DBF00210559 /* SignalMessaging */;
|
target = 453518911FC63DBF00210559 /* SignalMessaging */;
|
||||||
targetProxy = C36B8705243C50B00049991D /* PBXContainerItemProxy */;
|
targetProxy = C36B8705243C50B00049991D /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
C3C2A5A5255385C100C340D1 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */;
|
||||||
|
targetProxy = C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
/* End PBXTargetDependency section */
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
|
@ -4499,6 +4718,132 @@
|
||||||
};
|
};
|
||||||
name = "App Store Release";
|
name = "App Store Release";
|
||||||
};
|
};
|
||||||
|
C3C2A5A8255385C100C340D1 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = A6344D429FFAC3B44E6A06FA /* Pods-SessionSnodeKit.debug.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
INFOPLIST_FILE = SessionSnodeKit/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";
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
C3C2A5A9255385C100C340D1 /* App Store Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = C022DD8E076866C6241610BF /* Pods-SessionSnodeKit.app store release.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEFINES_MODULE = YES;
|
||||||
|
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||||
|
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||||
|
DYLIB_CURRENT_VERSION = 1;
|
||||||
|
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
INFOPLIST_FILE = SessionSnodeKit/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";
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
VERSION_INFO_PREFIX = "";
|
||||||
|
};
|
||||||
|
name = "App Store Release";
|
||||||
|
};
|
||||||
D221A0BA169C9E5F00537ABF /* Debug */ = {
|
D221A0BA169C9E5F00537ABF /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
@ -4933,6 +5278,15 @@
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = "App Store Release";
|
defaultConfigurationName = "App Store Release";
|
||||||
};
|
};
|
||||||
|
C3C2A5AA255385C100C340D1 /* Build configuration list for PBXNativeTarget "SessionSnodeKit" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
C3C2A5A8255385C100C340D1 /* Debug */,
|
||||||
|
C3C2A5A9255385C100C340D1 /* App Store Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = "App Store Release";
|
||||||
|
};
|
||||||
D221A083169C9E5E00537ABF /* Build configuration list for PBXProject "Signal" */ = {
|
D221A083169C9E5E00537ABF /* Build configuration list for PBXProject "Signal" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|
Loading…
Reference in New Issue