Create SessionSnodeKit

This commit is contained in:
nielsandriesse 2020-11-05 12:07:21 +11:00
parent a46630c192
commit 2b1e322832
27 changed files with 1720 additions and 2 deletions

15
Podfile
View File

@ -105,9 +105,16 @@ target 'SignalMessaging' do
shared_pods
end
target 'SessionSnodeKit' do
pod 'CryptoSwift', :inhibit_warnings => true
pod 'Curve25519Kit', :inhibit_warnings => true
pod 'PromiseKit', :inhibit_warnings => true
end
post_install do |installer|
enable_whole_module_optimization_for_cryptoswift(installer)
enable_extension_support_for_purelayout(installer)
set_minimum_deployment_target(installer)
end
def enable_whole_module_optimization_for_cryptoswift(installer)
@ -133,3 +140,11 @@ def enable_extension_support_for_purelayout(installer)
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

View File

@ -18,6 +18,7 @@ PODS:
- CocoaLumberjack/Core (= 3.6.2)
- CocoaLumberjack/Core (3.6.2)
- CryptoSwift (1.3.2)
- Curve25519Kit (2.1.0)
- FeedKit (8.1.1)
- GRKOpenSSLFramework (1.0.2.12)
- libPhoneNumber-iOS (0.9.15)
@ -199,11 +200,14 @@ PODS:
DEPENDENCIES:
- AFNetworking (~> 3.2.1)
- CryptoSwift
- CryptoSwift (~> 1.3)
- Curve25519Kit
- FeedKit (~> 8.1)
- GRKOpenSSLFramework (from `https://github.com/signalapp/GRKOpenSSLFramework`)
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
- NVActivityIndicatorView (~> 4.7)
- PromiseKit
- PromiseKit (= 6.5.3)
- PureLayout (~> 3.1.4)
- Reachability
@ -232,6 +236,7 @@ SPEC REPOS:
- AFNetworking
- CocoaLumberjack
- CryptoSwift
- Curve25519Kit
- FeedKit
- libPhoneNumber-iOS
- NVActivityIndicatorView
@ -309,6 +314,7 @@ SPEC CHECKSUMS:
AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057
CocoaLumberjack: bd155f2dd06c0e0b03f876f7a3ee55693122ec94
CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060
Curve25519Kit: 76d0859ecb34704f7732847812363f83b23a6a59
FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9
GRKOpenSSLFramework: 8a3735ad41e7dc1daff460467bccd32ca5d6ae3e
libPhoneNumber-iOS: 0a32a9525cf8744fe02c5206eb30d571e38f7d75
@ -333,6 +339,6 @@ SPEC CHECKSUMS:
YYImage: 6db68da66f20d9f169ceb94dfb9947c3867b9665
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: 8b58cc2d282fa528aa81b128b643596a9059c270
PODFILE CHECKSUM: a046210e9d91429c33c0fa60f7ff0362c6994f5d
COCOAPODS: 1.10.0.rc.1

2
Pods

@ -1 +1 @@
Subproject commit d1b2c2c2fe1b47ab1314192e9320f6cbc30871be
Subproject commit 5ba5f8d5a001bbf4d925ef0f246bf402e03b097d

View File

@ -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
}
}

108
SessionSnodeKit/HTTP.swift Normal file
View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -0,0 +1,4 @@
#import <Foundation/Foundation.h>
FOUNDATION_EXPORT double SessionSnodeKitVersionNumber;
FOUNDATION_EXPORT const unsigned char SessionSnodeKitVersionString[];

View File

@ -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")
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)" }
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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())
}
}

View File

@ -0,0 +1,7 @@
internal extension Array where Element : CustomStringConvertible {
var prettifiedDescription: String {
return "[ " + map { $0.description }.joined(separator: ", ") + " ]"
}
}

View File

@ -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)
}
}
}
}

View File

@ -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: ", ") + " ]"
}
}

View File

@ -0,0 +1,2 @@
public typealias JSON = [String:Any]

View File

@ -0,0 +1,6 @@
internal func SNLog(_ message: String) {
#if DEBUG
print("[Session] \(message)")
#endif
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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])
}
}

View File

@ -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
}

View File

@ -469,6 +469,7 @@
7BDCFC092421894900641C39 /* MessageFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 452ECA4C1E087E7200E2F016 /* MessageFetcherJob.swift */; };
7BDCFC0B2421EB7600641C39 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
7BF3FF002505B8E400609570 /* PlaceholderIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BF3FEFF2505B8E400609570 /* PlaceholderIcon.swift */; };
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */; };
A10FDF79184FB4BB007FF963 /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
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 */; };
C39DD28A24F3336E008590FC /* 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 */; };
C3D0972B2510499C00F6E3E4 /* BackgroundPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */; };
C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */; };
@ -677,6 +702,13 @@
remoteGlobalIDString = 453518911FC63DBF00210559;
remoteInfo = SignalMessaging;
};
C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = D221A080169C9E5E00537ABF /* Project object */;
proxyType = 1;
remoteGlobalIDString = C3C2A59E255385C100C340D1;
remoteInfo = SessionSnodeKit;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -698,6 +730,7 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
C3C2A5A7255385C100C340D1 /* SessionSnodeKit.framework in Embed Frameworks */,
4535189A1FC63DBF00210559 /* SignalMessaging.framework in 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>"; };
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>"; };
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>"; };
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; };
@ -1265,6 +1299,7 @@
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; };
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>"; };
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>"; };
@ -1363,6 +1398,7 @@
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>"; };
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>"; };
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>"; };
@ -1400,6 +1436,30 @@
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>"; };
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>"; };
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>"; };
@ -1462,6 +1522,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C3C2A59C255385C100C340D1 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
9EE44C6B4D4A069B86112387 /* Pods_SessionSnodeKit.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
D221A086169C9E5E00537ABF /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -1481,6 +1549,7 @@
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */,
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */,
453518991FC63DBF00210559 /* SignalMessaging.framework in Frameworks */,
C3C2A5A6255385C100C340D1 /* SessionSnodeKit.framework in Frameworks */,
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */,
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */,
768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */,
@ -2497,6 +2566,8 @@
8EEE74B0753448C085B48721 /* Pods-SignalMessaging.app store release.xcconfig */,
F62ECF7B8AF4F8089AA705B3 /* Pods-LokiPushNotificationService.debug.xcconfig */,
18D19142FD6E60FD0A5D89F7 /* Pods-LokiPushNotificationService.app store release.xcconfig */,
A6344D429FFAC3B44E6A06FA /* Pods-SessionSnodeKit.debug.xcconfig */,
C022DD8E076866C6241610BF /* Pods-SessionSnodeKit.app store release.xcconfig */,
);
name = Pods;
sourceTree = "<group>";
@ -2803,6 +2874,52 @@
path = SwiftCSV;
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 = {
isa = PBXGroup;
children = (
@ -2810,6 +2927,7 @@
453518691FC635DD00210559 /* SignalShareExtension */,
453518931FC63DBF00210559 /* SignalMessaging */,
7BC01A3C241F40AB00BC7C55 /* LokiPushNotificationService */,
C3C2A5A0255385C100C340D1 /* SessionSnodeKit */,
D221A08C169C9E5E00537ABF /* Frameworks */,
D221A08A169C9E5E00537ABF /* Products */,
9404664EC513585B05DF1350 /* Pods */,
@ -2824,6 +2942,7 @@
453518681FC635DD00210559 /* SignalShareExtension.appex */,
453518921FC63DBF00210559 /* SignalMessaging.framework */,
7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */,
C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */,
);
name = Products;
sourceTree = "<group>";
@ -2871,6 +2990,7 @@
748A5CAEDD7C919FC64C6807 /* Pods_SignalTests.framework */,
264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */,
04912E453971FB16E5E78EC6 /* Pods_LokiPushNotificationService.framework */,
9559C3068280BA2383F547F7 /* Pods_SessionSnodeKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@ -2988,6 +3108,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C3C2A59A255385C100C340D1 /* Headers */ = {
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
C3C2A5A3255385C100C340D1 /* SessionSnodeKit.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXHeadersBuildPhase section */
/* Begin PBXNativeTarget section */
@ -3048,6 +3176,25 @@
productReference = 7BC01A3B241F40AB00BC7C55 /* LokiPushNotificationService.appex */;
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 */ = {
isa = PBXNativeTarget;
buildConfigurationList = D221A0BC169C9E5F00537ABF /* Build configuration list for PBXNativeTarget "Signal" */;
@ -3068,6 +3215,7 @@
453518711FC635DD00210559 /* PBXTargetDependency */,
453518981FC63DBF00210559 /* PBXTargetDependency */,
7BC01A41241F40AB00BC7C55 /* PBXTargetDependency */,
C3C2A5A5255385C100C340D1 /* PBXTargetDependency */,
);
name = Signal;
productName = RedPhone;
@ -3139,6 +3287,12 @@
DevelopmentTeam = SUQ8J2PCT7;
ProvisioningStyle = Automatic;
};
C3C2A59E255385C100C340D1 = {
CreatedOnToolsVersion = 12.1;
DevelopmentTeam = SUQ8J2PCT7;
LastSwiftMigration = 1210;
ProvisioningStyle = Automatic;
};
D221A088169C9E5E00537ABF = {
DevelopmentTeam = SUQ8J2PCT7;
LastSwiftMigration = 1020;
@ -3208,6 +3362,7 @@
453518671FC635DD00210559 /* SignalShareExtension */,
453518911FC63DBF00210559 /* SignalMessaging */,
7BC01A3A241F40AB00BC7C55 /* LokiPushNotificationService */,
C3C2A59E255385C100C340D1 /* SessionSnodeKit */,
);
};
/* End PBXProject section */
@ -3243,6 +3398,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
C3C2A59D255385C100C340D1 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
D221A087169C9E5E00537ABF /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -3472,6 +3634,7 @@
"${BUILT_PRODUCTS_DIR}/YapDatabase/YapDatabase.framework",
"${BUILT_PRODUCTS_DIR}/ZXingObjC/ZXingObjC.framework",
"${BUILT_PRODUCTS_DIR}/libPhoneNumber-iOS/libPhoneNumber_iOS.framework",
"${BUILT_PRODUCTS_DIR}/Curve25519Kit/Curve25519Kit.framework",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
@ -3501,6 +3664,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YapDatabase.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ZXingObjC.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libPhoneNumber_iOS.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Curve25519Kit.framework",
);
runOnlyForDeploymentPostprocessing = 0;
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";
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 */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@ -3776,6 +3962,34 @@
);
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 */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -4102,6 +4316,11 @@
target = 453518911FC63DBF00210559 /* SignalMessaging */;
targetProxy = C36B8705243C50B00049991D /* PBXContainerItemProxy */;
};
C3C2A5A5255385C100C340D1 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = C3C2A59E255385C100C340D1 /* SessionSnodeKit */;
targetProxy = C3C2A5A4255385C100C340D1 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -4499,6 +4718,132 @@
};
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 */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -4933,6 +5278,15 @@
defaultConfigurationIsVisible = 0;
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" */ = {
isa = XCConfigurationList;
buildConfigurations = (