Merge branch 'dev' into push-notifications

pull/152/head
gmbnt 4 years ago
commit 03c4e4a65c

@ -5035,7 +5035,7 @@
baseConfigurationReference = AD2AB1207E8888E4262D781B /* Pods-SignalTests.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Signal.app/Signal";
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Session.app/Session";
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
@ -5067,7 +5067,7 @@
"\"$(TARGET_TEMP_DIR)/../$(PROJECT_NAME).build/DerivedSources\"",
);
INFOPLIST_FILE = "Signal/test/Supporting Files/SignalTests-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@ -5094,7 +5094,7 @@
baseConfigurationReference = E85DB184824BA9DC302EC8B3 /* Pods-SignalTests.app store release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Signal.app/Signal";
BUNDLE_LOADER = "$(BUILT_PRODUCTS_DIR)/Session.app/Session";
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "iPhone Developer";
@ -5125,7 +5125,7 @@
"\"$(TARGET_TEMP_DIR)/../$(PROJECT_NAME).build/DerivedSources\"",
);
INFOPLIST_FILE = "Signal/test/Supporting Files/SignalTests-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",

@ -77,6 +77,104 @@
BlueprintName = "SignalServiceKit-Unit-Tests"
ReferencedContainer = "container:Pods/Pods.xcodeproj">
</BuildableReference>
<SkippedTests>
<Test
Identifier = "ContactSortingTest">
</Test>
<Test
Identifier = "DeviceNamesTest">
</Test>
<Test
Identifier = "JobQueueTest">
</Test>
<Test
Identifier = "MessageSenderJobQueueTest">
</Test>
<Test
Identifier = "OWSAnalyticsTests">
</Test>
<Test
Identifier = "OWSDeviceProvisionerTest">
</Test>
<Test
Identifier = "OWSDisappearingMessageFinderTest">
</Test>
<Test
Identifier = "OWSDisappearingMessagesConfigurationTest">
</Test>
<Test
Identifier = "OWSDisappearingMessagesJobTest">
</Test>
<Test
Identifier = "OWSFingerprintTest">
</Test>
<Test
Identifier = "OWSIncomingMessageFinderTest">
</Test>
<Test
Identifier = "OWSLinkPreviewTest">
</Test>
<Test
Identifier = "OWSMessageManagerTest">
</Test>
<Test
Identifier = "OWSMessageSenderTest">
</Test>
<Test
Identifier = "OWSProvisioningCipherTest">
</Test>
<Test
Identifier = "OWSSignalAddressTest">
</Test>
<Test
Identifier = "OWSUDManagerTest">
</Test>
<Test
Identifier = "PhoneNumberTest">
</Test>
<Test
Identifier = "PhoneNumberUtilTest">
</Test>
<Test
Identifier = "SSKBaseTestObjC">
</Test>
<Test
Identifier = "SSKBaseTestSwift">
</Test>
<Test
Identifier = "SSKMessageSenderJobRecordTest">
</Test>
<Test
Identifier = "SignalRecipientTest">
</Test>
<Test
Identifier = "SignedPreKeyDeletionTests">
</Test>
<Test
Identifier = "TSContactThreadTest">
</Test>
<Test
Identifier = "TSGroupThreadTest">
</Test>
<Test
Identifier = "TSMessageStorageTests">
</Test>
<Test
Identifier = "TSMessageTest">
</Test>
<Test
Identifier = "TSOutgoingMessageTest">
</Test>
<Test
Identifier = "TSStorageIdentityKeyStoreTests">
</Test>
<Test
Identifier = "TSStoragePreKeyStoreTests">
</Test>
<Test
Identifier = "TSThreadTest">
</Test>
</SkippedTests>
</TestableReference>
<TestableReference
skipped = "NO">

@ -7,7 +7,7 @@
<key>CarthageVersion</key>
<string>0.34.0</string>
<key>OSXVersion</key>
<string>10.15.3</string>
<string>10.15.4</string>
<key>WebRTCCommit</key>
<string>1445d719bf05280270e9f77576f80f973fd847f8 M73</string>
</dict>
@ -83,7 +83,7 @@
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>imaginary.stream</key>
<key>public.loki.foundation</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
@ -113,7 +113,7 @@
<key>NSContactsUsageDescription</key>
<string>Signal uses your contacts to find users you know. We do not store your contacts on the server.</string>
<key>NSFaceIDUsageDescription</key>
<string>Session's Screen Lock feature uses Face ID.</string>
<string>Session&apos;s Screen Lock feature uses Face ID.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Session needs access to your microphone to record videos.</string>
<key>NSPhotoLibraryAddUsageDescription</key>

@ -1181,11 +1181,9 @@ static BOOL isUsingFullAPNs = YES;
PMKJoin(promises).then(^(id results) {
completionHandler(UIBackgroundFetchResultNewData);
CurrentAppContext().wasWokenUpBySilentPushNotification = false;
[LKLogger print:@"[Loki] UIBackgroundFetchResultNewData"];
}).catch(^(id error) {
completionHandler(UIBackgroundFetchResultFailed);
CurrentAppContext().wasWokenUpBySilentPushNotification = false;
[LKLogger print:@"[Loki] UIBackgroundFetchResultFailed"];
});
}

@ -5,7 +5,7 @@
import XCTest
import PromiseKit
import SignalServiceKit
@testable import Signal
@testable import Session
struct VerificationFailedError: Error { }
struct FailedToGetRPRegistrationTokenError: Error { }

@ -3,7 +3,7 @@
//
import XCTest
import Signal
import Session
class PhoneNumberValidatorTest: SignalBaseTest {

@ -13,6 +13,7 @@
#import <SignalServiceKit/TSOutgoingMessage.h>
#import <YapDatabase/YapDatabaseConnection.h>
/*
@interface ConversationViewItemTest : SignalBaseTest
@property TSThread *thread;
@ -271,3 +272,4 @@
}
@end
*/

@ -4,8 +4,9 @@
import XCTest
import WebRTC
@testable import Signal
@testable import Session
/*
/**
* Playing the role of the call service.
*/
@ -135,3 +136,4 @@ class PeerConnectionClientTest: SignalBaseTest {
XCTAssertEqual(123, hangupProto.id)
}
}
*/

@ -4,7 +4,7 @@
import XCTest
import Contacts
@testable import Signal
@testable import Session
final class ContactsPickerTest: SignalBaseTest {
private var prevLang: Any?

@ -3,7 +3,7 @@
//
import XCTest
@testable import Signal
@testable import Session
class ByteParserTest: SignalBaseTest {

@ -3,7 +3,7 @@
//
import XCTest
@testable import Signal
@testable import Session
@testable import SignalMessaging
class DisplayableTextTest: SignalBaseTest {

@ -3,7 +3,7 @@
//
import XCTest
@testable import Signal
@testable import Session
class ImageCacheTest: SignalBaseTest {

@ -3,7 +3,8 @@
//
import XCTest
@testable import Signal
import Contacts
@testable import Session
@testable import SignalMessaging
// TODO: We might be able to merge this with OWSFakeContactsManager.
@ -74,6 +75,7 @@ class FullTextSearcherTest: SignalBaseTest {
super.tearDown()
}
/*
override func setUp() {
super.setUp()
@ -428,4 +430,5 @@ class SearcherTest: SignalBaseTest {
XCTAssertEqual(FullTextSearchFinder.normalize(text: "renaldo RENALDO reñaldo REÑALDO"), "renaldo RENALDO reñaldo REÑALDO")
XCTAssertEqual(FullTextSearchFinder.normalize(text: "😏"), "😏")
}
*/
}

@ -3,7 +3,7 @@
//
import XCTest
@testable import Signal
@testable import Session
@testable import SignalMessaging
class ImageEditorModelTest: SignalBaseTest {

@ -3,7 +3,7 @@
//
import XCTest
@testable import Signal
@testable import Session
@testable import SignalMessaging
extension ImageEditorModel {

@ -780,7 +780,7 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa
- (void)saveFriendRequestStatus:(LKThreadFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction
{
self.friendRequestStatus = friendRequestStatus;
NSLog(@"[Loki] Setting thread friend request status to %@.", self.friendRequestStatusDescription);
[LKLogger print:[NSString stringWithFormat:@"[Loki] Setting thread friend request status to %@.", self.friendRequestStatusDescription]];
void (^postNotification)() = ^() {
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.threadFriendRequestStatusChanged object:self.uniqueId];
};

@ -0,0 +1,93 @@
import PromiseKit
internal enum HTTP {
private static let urlSession = URLSession(configuration: .ephemeral, delegate: urlSessionDelegate, delegateQueue: nil)
private static let urlSessionDelegate = URLSessionDelegateImplementation()
// MARK: Settings
private static let timeout: TimeInterval = 20
// MARK: URL Session Delegate Implementation
private final class URLSessionDelegateImplementation : 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
internal enum Verb : String {
case get = "GET"
case put = "PUT"
case post = "POST"
case delete = "DELETE"
}
// MARK: Error
internal enum Error : LocalizedError {
case generic
case httpRequestFailed(statusCode: UInt, json: JSON?)
case invalidJSON
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
internal static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<JSON> {
return Promise<JSON> { seal in
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
if let parameters = parameters {
do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(Error.invalidJSON) }
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch (let error) {
return seal.reject(error)
}
}
request.timeoutInterval = timeout
let task = urlSession.dataTask(with: request) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse else {
if let error = error {
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
} else {
print("[Loki] \(verb.rawValue) request to \(url) failed.")
}
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
}
if let error = error {
print("[Loki] \(verb.rawValue) request to \(url) failed due to error: \(error).")
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, json: nil))
}
let statusCode = UInt(response.statusCode)
var json: JSON? = nil
if let j = try? JSONSerialization.jsonObject(with: data, options: []) as? JSON {
json = j
} else if let result = String(data: data, encoding: .utf8) {
json = [ "result" : result ]
}
guard 200...299 ~= statusCode else {
let jsonDescription = json?.prettifiedDescription ?? "no debugging info provided"
print("[Loki] \(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, json: json))
}
if let json = json {
seal.fulfill(json)
} else {
print("[Loki] Couldn't parse JSON returned by \(verb.rawValue) request to \(url).")
return seal.reject(Error.invalidJSON)
}
}
task.resume()
}
}
}

@ -3,13 +3,14 @@ import PromiseKit
public extension LokiAPI {
private static var snodeVersion: [LokiAPITarget:String] = [:]
/// Only ever accessed from `LokiAPI.errorHandlingQueue` to avoid race conditions.
fileprivate static var failureCount: [LokiAPITarget:UInt] = [:]
/// Only ever modified from `LokiAPI.errorHandlingQueue` to avoid race conditions.
internal static var failureCount: [LokiAPITarget:UInt] = [:]
// MARK: Settings
private static let minimumSnodeCount = 2
private static let targetSnodeCount = 3
fileprivate static let failureThreshold = 2
internal static let failureThreshold = 2
// MARK: Caching
internal static var swarmCache: [String:[LokiAPITarget]] = [:]
@ -23,8 +24,13 @@ public extension LokiAPI {
}
// MARK: Clearnet Setup
#if TESTNET
fileprivate static let seedNodePool: Set<String> = [ "http://public.loki.foundation:38157" ]
#else
fileprivate static let seedNodePool: Set<String> = [ "http://storage.seed1.loki.network:22023", "http://storage.seed2.loki.network:38157", "http://149.56.148.124:38157" ]
fileprivate static var randomSnodePool: Set<LokiAPITarget> = []
#endif
internal static var randomSnodePool: Set<LokiAPITarget> = []
@objc public static func clearRandomSnodePool() {
randomSnodePool.removeAll()
@ -35,36 +41,40 @@ public extension LokiAPI {
// All of this has to happen on DispatchQueue.global() due to the way OWSMessageManager works
if randomSnodePool.isEmpty {
let target = seedNodePool.randomElement()!
let url = URL(string: "\(target)/json_rpc")!
let request = TSRequest(url: url, method: "POST", parameters: [
"method" : "get_service_nodes",
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
"pubkey_ed25519" : true,
"pubkey_x25519" : true
]
]
])
print("[Loki] Invoking get_service_nodes on \(target).")
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global()).map(on: DispatchQueue.global()) { intermediate in
let rawResponse = intermediate.responseObject
guard let json = rawResponse as? JSON, let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
]
print("[Loki] Populating snode pool using: \(target).")
let (promise, seal) = Promise<LokiAPITarget>.pending()
let queue = DispatchQueue.global()
HTTP.execute(.post, url, parameters: parameters).map(on: queue) { json in
guard let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let idKey = rawTarget["pubkey_ed25519"] as? String, let encryptionKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
return nil
}
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(idKey: idKey, encryptionKey: encryptionKey))
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
return randomSnodePool.randomElement()!
}.recover(on: DispatchQueue.global()) { error -> Promise<LokiAPITarget> in
}.retryingIfNeeded(maxRetryCount: 4).done(on: queue) { snode in
seal.fulfill(snode)
}.catch(on: queue) { error in
print("[Loki] Failed to contact seed node at: \(target).")
throw error
}.retryingIfNeeded(maxRetryCount: 16) // The seed nodes have historically been unreliable
seal.reject(error)
}
return promise
} else {
return Promise<LokiAPITarget> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
@ -114,7 +124,7 @@ public extension LokiAPI {
print("[Loki] Rejecting file server proxy with version number \(version).")
return getFileServerProxy()
}
}.recover(on: DispatchQueue.global()) { error in
}.recover(on: DispatchQueue.global()) { _ in
return getFileServerProxy()
}
}.done(on: DispatchQueue.global()) { snode in
@ -132,19 +142,19 @@ public extension LokiAPI {
return []
}
return rawTargets.flatMap { rawTarget in
guard let address = rawTarget["ip"] as? String, let portAsString = rawTarget["port"] as? String, let port = UInt16(portAsString), let idKey = rawTarget["pubkey_ed25519"] as? String, let encryptionKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
guard let address = rawTarget["ip"] as? String, let portAsString = rawTarget["port"] as? String, let port = UInt16(portAsString), let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
return nil
}
return LokiAPITarget(address: "https://\(address)", port: port, publicKeySet: LokiAPITarget.KeySet(idKey: idKey, encryptionKey: encryptionKey))
return LokiAPITarget(address: "https://\(address)", port: port, publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
}
}
}
// MARK: Error Handling
// MARK: Snode Error Handling
internal extension Promise {
internal func handlingSwarmSpecificErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
internal func handlingSnodeErrorsIfNeeded(for target: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<T> {
return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise<T> in
if let error = error as? LokiHTTPClient.HTTPError {
switch error.statusCode {

@ -2,7 +2,7 @@ import PromiseKit
@objc(LKAPI)
public final class LokiAPI : NSObject {
private static let stateQueue = DispatchQueue(label: "stateQueue")
private static let stateQueue = DispatchQueue(label: "LokiAPI.stateQueue")
/// Only ever modified from the message processing queue (`OWSBatchMessageProcessor.processingQueue`).
private static var syncMessageTimestamps: [String:Set<UInt64>] = [:]
@ -22,19 +22,21 @@ public final class LokiAPI : NSObject {
}
/// All service node related errors must be handled on this queue to avoid race conditions maintaining e.g. failure counts.
public static let errorHandlingQueue = DispatchQueue(label: "errorHandlingQueue")
public static let errorHandlingQueue = DispatchQueue(label: "LokiAPI.errorHandlingQueue")
// MARK: Convenience
internal static let storage = OWSPrimaryStorage.shared()
internal static let userHexEncodedPublicKey = getUserHexEncodedPublicKey()
// MARK: Settings
private static let apiVersion = "v1"
private static let maxRetryCount: UInt = 8
private static let useOnionRequests = true
private static let maxRetryCount: UInt = 4
private static let defaultTimeout: TimeInterval = 20
private static let longPollingTimeout: TimeInterval = 40
private static var userIDScanLimit: UInt = 4096
internal static var powDifficulty: UInt = 4
internal static var powDifficulty: UInt = 2
public static let defaultMessageTTL: UInt64 = 24 * 60 * 60 * 1000
public static let deviceLinkUpdateInterval: TimeInterval = 20
@ -93,17 +95,19 @@ public final class LokiAPI : NSObject {
// MARK: Internal API
internal static func invoke(_ method: LokiAPITarget.Method, on target: LokiAPITarget, associatedWith hexEncodedPublicKey: String,
parameters: [String:Any], headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/\(apiVersion)")!
parameters: JSON, headers: [String:String]? = nil, timeout: TimeInterval? = nil) -> RawResponsePromise {
let url = URL(string: "\(target.address):\(target.port)/storage_rpc/v1")!
let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ])
if let headers = headers { request.allHTTPHeaderFields = headers }
request.timeoutInterval = timeout ?? defaultTimeout
let headers = request.allHTTPHeaderFields ?? [:]
let headersDescription = headers.isEmpty ? "no custom headers specified" : headers.prettifiedDescription
print("[Loki] Invoking \(method.rawValue) on \(target) with \(parameters.prettifiedDescription) (\(headersDescription)).")
return LokiSnodeProxy(for: target).perform(request, withCompletionQueue: DispatchQueue.global())
.handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
.recoveringNetworkErrorsIfNeeded()
if useOnionRequests {
return OnionRequestAPI.sendOnionRequest(invoking: method, on: target, with: parameters, associatedWith: hexEncodedPublicKey).map { $0 as Any }
} else {
return TSNetworkManager.shared().perform(request, withCompletionQueue: DispatchQueue.global())
.map { $0.responseObject }
.handlingSnodeErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey)
.recoveringNetworkErrorsIfNeeded()
}
}
internal static func getRawMessages(from target: LokiAPITarget, usingLongPolling useLongPolling: Bool) -> RawResponsePromise {
@ -202,7 +206,7 @@ public final class LokiAPI : NSObject {
print("[Loki] Failed to update proof of work difficulty from: \(rawResponse).")
}
return rawResponse
}
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
})
}.retryingIfNeeded(maxRetryCount: maxRetryCount)
}
@ -259,11 +263,6 @@ public final class LokiAPI : NSObject {
let newRawMessages = removeDuplicates(from: rawMessages)
let newMessages = parseProtoEnvelopes(from: newRawMessages)
let newMessageCount = newMessages.count
if newMessageCount == 1 {
print("[Loki] Retrieved 1 new message.")
} else if (newMessageCount != 0) {
print("[Loki] Retrieved \(newMessageCount) new messages.")
}
return newMessages
}

@ -11,11 +11,12 @@ internal final class LokiAPITarget : NSObject, NSCoding {
/// Only supported by snode targets.
case getMessages = "retrieve"
case sendMessage = "store"
case getStats = "get_stats"
}
internal struct KeySet {
let idKey: String
let encryptionKey: String
let ed25519Key: String
let x25519Key: String
}
// MARK: Initialization
@ -30,7 +31,7 @@ internal final class LokiAPITarget : NSObject, NSCoding {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
if let idKey = coder.decodeObject(forKey: "idKey") as? String, let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String {
publicKeySet = KeySet(idKey: idKey, encryptionKey: encryptionKey)
publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey)
} else {
publicKeySet = nil
}
@ -41,8 +42,8 @@ internal final class LokiAPITarget : NSObject, NSCoding {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
if let keySet = publicKeySet {
coder.encode(keySet.idKey, forKey: "idKey")
coder.encode(keySet.encryptionKey, forKey: "encryptionKey")
coder.encode(keySet.ed25519Key, forKey: "idKey")
coder.encode(keySet.x25519Key, forKey: "encryptionKey")
}
}

@ -56,7 +56,6 @@ internal class LokiFileServerProxy : LokiHTTPClient {
serverURLEndIndex < urlAsString.endIndex else { throw Error.endpointParsingFailed }
let endpointStartIndex = urlAsString.index(after: serverURLEndIndex)
let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex])
print("[Loki] Proxying file server request (\(endpoint)) through \(proxy).")
let parametersAsString: String
if let tsRequest = request as? TSRequest {
headers["Content-Type"] = "application/json"

@ -60,7 +60,6 @@ public final class LokiPoller : NSObject {
// randomElement() uses the system's default random generator, which is cryptographically secure
let nextSnode = unusedSnodes.randomElement()!
usedSnodes.insert(nextSnode)
print("[Loki] Polling \(nextSnode).")
poll(nextSnode, seal: seal).done(on: DispatchQueue.global()) {
seal.fulfill(())
}.catch(on: LokiAPI.errorHandlingQueue) { [weak self] error in

@ -1,99 +0,0 @@
import PromiseKit
import SignalMetadataKit
internal class LokiSnodeProxy : LokiHTTPClient {
private let target: LokiAPITarget
private let keyPair = Curve25519.generateKeyPair()
// MARK: Error
internal enum Error : LocalizedError {
case targetPublicKeySetMissing
case symmetricKeyGenerationFailed
case proxyResponseParsingFailed
case targetSnodeHTTPError(code: Int, message: Any?)
internal var errorDescription: String? {
switch self {
case .targetPublicKeySetMissing: return "Missing target public key set."
case .symmetricKeyGenerationFailed: return "Couldn't generate symmetric key."
case .proxyResponseParsingFailed: return "Couldn't parse snode proxy response."
case .targetSnodeHTTPError(let httpStatusCode, let message): return "Target snode returned error \(httpStatusCode) with description: \(message ?? "no description provided.")."
}
}
}
// MARK: Initialization
internal init(for target: LokiAPITarget) {
self.target = target
super.init()
}
// MARK: Proxying
override internal func perform(_ request: TSRequest, withCompletionQueue queue: DispatchQueue = DispatchQueue.main) -> LokiAPI.RawResponsePromise {
guard let targetHexEncodedPublicKeySet = target.publicKeySet else { return Promise(error: Error.targetPublicKeySetMissing) }
let headers = getCanonicalHeaders(for: request)
return Promise<LokiAPI.RawResponse> { [target = self.target, keyPair = self.keyPair, httpSession = self.httpSession] seal in
DispatchQueue.global().async {
let uncheckedSymmetricKey = try? Curve25519.generateSharedSecret(fromPublicKey: Data(hex: targetHexEncodedPublicKeySet.encryptionKey), privateKey: keyPair.privateKey)
guard let symmetricKey = uncheckedSymmetricKey else { return seal.reject(Error.symmetricKeyGenerationFailed) }
LokiAPI.getRandomSnode().then(on: DispatchQueue.global()) { proxy -> Promise<LokiAPI.RawResponse> in
let url = "\(proxy.address):\(proxy.port)/proxy"
print("[Loki] Proxying request to \(target) through \(proxy).")
let parametersAsData = try JSONSerialization.data(withJSONObject: request.parameters, options: [])
let proxyRequestParameters: JSON = [
"method" : request.httpMethod,
"body" : String(bytes: parametersAsData, encoding: .utf8),
"headers" : headers
]
let proxyRequestParametersAsData = try JSONSerialization.data(withJSONObject: proxyRequestParameters, options: [])
let ivAndCipherText = try DiffieHellman.encrypt(proxyRequestParametersAsData, using: symmetricKey)
let proxyRequestHeaders = [
"X-Sender-Public-Key" : keyPair.publicKey.toHexString(),
"X-Target-Snode-Key" : targetHexEncodedPublicKeySet.idKey
]
let (promise, resolver) = LokiAPI.RawResponsePromise.pending()
let proxyRequest = AFHTTPRequestSerializer().request(withMethod: "POST", urlString: url, parameters: nil, error: nil)
proxyRequest.allHTTPHeaderFields = proxyRequestHeaders
proxyRequest.httpBody = ivAndCipherText
proxyRequest.timeoutInterval = request.timeoutInterval
var task: URLSessionDataTask!
task = httpSession.dataTask(with: proxyRequest as URLRequest) { response, result, error in
if let error = error {
let nmError = NetworkManagerError.taskError(task: task, underlyingError: error)
let nsError: NSError = nmError as NSError
nsError.isRetryable = false
resolver.reject(nsError)
} else {
resolver.fulfill(result)
}
}
task.resume()
return promise
}.map(on: DispatchQueue.global()) { rawResponse in
guard let responseAsData = rawResponse as? Data, let cipherText = Data(base64Encoded: responseAsData) else {
print("[Loki] Received a non-string encoded response.")
return rawResponse
}
let response = try DiffieHellman.decrypt(cipherText, using: symmetricKey)
let uncheckedJSON = try? JSONSerialization.jsonObject(with: response, options: .allowFragments) as? JSON
guard let json = uncheckedJSON, let statusCode = json["status"] as? Int else { throw HTTPError.networkError(code: -1, response: nil, underlyingError: Error.proxyResponseParsingFailed) }
let isSuccess = (200...299) ~= statusCode
var body: Any? = nil
if let bodyAsString = json["body"] as? String {
body = bodyAsString
if let bodyAsJSON = try? JSONSerialization.jsonObject(with: bodyAsString.data(using: .utf8)!, options: .allowFragments) as? JSON {
body = bodyAsJSON
}
}
guard isSuccess else { throw HTTPError.networkError(code: statusCode, response: body, underlyingError: Error.targetSnodeHTTPError(code: statusCode, message: body)) }
return body
}.done { rawResponse in
seal.fulfill(rawResponse)
}.catch { error in
print("[Loki] Proxy request failed with error: \(error.localizedDescription).")
seal.reject(HTTPError.from(error: error) ?? error)
}
}
}
}
}

@ -0,0 +1,86 @@
import CryptoSwift
import PromiseKit
extension OnionRequestAPI {
internal static let gcmTagSize: UInt = 16
internal static let ivSize: UInt = 12
internal typealias EncryptionResult = (ciphertext: Data, symmetricKey: Data, ephemeralPublicKey: Data)
private static func getQueue() -> DispatchQueue {
return DispatchQueue(label: UUID().uuidString, qos: .userInitiated)
}
/// 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.
private static func getSecureRandomData(ofSize size: UInt) throws -> Data {
var data = Data(count: Int(size))
let result = data.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, Int(size), $0.baseAddress!) }
guard result == errSecSuccess else { throw Error.randomDataGenerationFailed }
return data
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, usingAESGCMWithSymmetricKey symmetricKey: Data) throws -> Data {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:usingAESGCMWithSymmetricKey:) from the main thread.") }
let iv = try getSecureRandomData(ofSize: ivSize)
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let aes = try AES(key: symmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let ciphertext = try aes.encrypt(plaintext.bytes)
return iv + Data(bytes: ciphertext)
}
/// - Note: Sync. Don't call from the main thread.
private static func encrypt(_ plaintext: Data, forSnode snode: LokiAPITarget) throws -> EncryptionResult {
guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") }
guard let hexEncodedSnodeX25519PublicKey = snode.publicKeySet?.x25519Key else { throw Error.snodePublicKeySetMissing }
let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey)
let ephemeralKeyPair = Curve25519.generateKeyPair()
let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: snodeX25519PublicKey, privateKey: ephemeralKeyPair.privateKey)
let key = "LOKI"
let symmetricKey = try HMAC(key: key.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey))
return (ciphertext, Data(bytes: symmetricKey), ephemeralKeyPair.publicKey)
}
/// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request.
internal static func encrypt(_ payload: JSON, forTargetSnode snode: LokiAPITarget) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
getQueue().async {
do {
guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) }
let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [])
let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [])
let result = try encrypt(plaintext, forSnode: snode)
seal.fulfill(result)
} catch (let error) {
seal.reject(error)
}
}
return promise
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
internal static func encryptHop(from lhs: LokiAPITarget, to rhs: LokiAPITarget, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending()
getQueue().async {
let parameters: JSON = [
"ciphertext" : previousEncryptionResult.ciphertext.base64EncodedString(),
"ephemeral_key" : previousEncryptionResult.ephemeralPublicKey.toHexString(),
"destination" : rhs.publicKeySet!.ed25519Key
]
do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [])
let result = try encrypt(plaintext, forSnode: lhs)
seal.fulfill(result)
} catch (let error) {
seal.reject(error)
}
}
return promise
}
}

@ -0,0 +1,274 @@
import CryptoSwift
import PromiseKit
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
internal enum OnionRequestAPI {
/// - Note: Exposed for testing purposes.
internal static let workQueue = DispatchQueue(label: "OnionRequestAPI.workQueue", qos: .userInitiated)
/// - Note: Must only be modified from `workQueue`.
internal static var guardSnodes: Set<LokiAPITarget> = []
/// - Note: Must only be modified from `workQueue`.
internal static var paths: Set<Path> = []
private static var snodePool: Set<LokiAPITarget> {
let unreliableSnodes = Set(LokiAPI.failureCount.keys)
return LokiAPI.randomSnodePool.subtracting(unreliableSnodes)
}
// MARK: Settings
private static let pathCount: UInt = 2
/// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 1
private static var guardSnodeCount: UInt { return pathCount } // One per path
// MARK: Error
internal enum Error : LocalizedError {
case httpRequestFailedAtTargetSnode(statusCode: UInt, json: JSON)
case insufficientSnodes
case missingSnodeVersion
case randomDataGenerationFailed
case snodePublicKeySetMissing
case unsupportedSnodeVersion(String)
var errorDescription: String? {
switch self {
case .httpRequestFailedAtTargetSnode(let statusCode): return "HTTP request failed at target snode with status code: \(statusCode)."
case .insufficientSnodes: return "Couldn't find enough snodes to build a path."
case .missingSnodeVersion: return "Missing snode version."
case .randomDataGenerationFailed: return "Couldn't generate random data."
case .snodePublicKeySetMissing: return "Missing snode public key set."
case .unsupportedSnodeVersion(let version): return "Unsupported snode version: \(version)."
}
}
}
// MARK: Path
internal typealias Path = [LokiAPITarget]
// MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: LokiAPITarget, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: 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: LokiAPITarget) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
let queue = DispatchQueue(label: UUID().uuidString, qos: .userInitiated) // No need to block the work queue for this
queue.async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 6 // Use a shorter timeout for testing
HTTP.execute(.get, url, timeout: timeout).done(on: queue) { rawResponse in
guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) }
if version >= "2.0.0" {
seal.fulfill(())
} else {
print("[Loki] [Onion Request API] Unsupported snode version: \(version).")
seal.reject(Error.unsupportedSnodeVersion(version))
}
}.catch(on: queue) { error in
seal.reject(error)
}
}
return promise
}
/// Finds `guardSnodeCount` 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() -> Promise<Set<LokiAPITarget>> {
if guardSnodes.count >= guardSnodeCount {
return Promise<Set<LokiAPITarget>> { $0.fulfill(guardSnodes) }
} else {
print("[Loki] [Onion Request API] Populating guard snode cache.")
return LokiAPI.getRandomSnode().then(on: workQueue) { _ -> Promise<Set<LokiAPITarget>> in // Just used to populate the snode pool
var unusedSnodes = snodePool // Sync on workQueue
guard unusedSnodes.count >= guardSnodeCount else { throw Error.insufficientSnodes }
func getGuardSnode() -> Promise<LokiAPITarget> {
// randomElement() uses the system's default random generator, which is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else { return Promise<LokiAPITarget> { $0.reject(Error.insufficientSnodes) } }
unusedSnodes.remove(candidate) // All used snodes should be unique
print("[Loki] [Onion Request API] Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate).map(on: workQueue) { candidate }.recover(on: workQueue) { _ in getGuardSnode() }
}
let promises = (0..<guardSnodeCount).map { _ in getGuardSnode() }
return when(fulfilled: promises).map(on: workQueue) { guardSnodes in
let guardSnodesAsSet = Set(guardSnodes)
OnionRequestAPI.guardSnodes = guardSnodesAsSet
return guardSnodesAsSet
}
}
}
}
/// Builds and returns `pathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
private static func buildPaths() -> Promise<Set<Path>> {
print("[Loki] [Onion Request API] Building onion request paths.")
return LokiAPI.getRandomSnode().then(on: workQueue) { _ -> Promise<Set<Path>> in // Just used to populate the snode pool
return getGuardSnodes().map(on: workQueue) { guardSnodes in
var unusedSnodes = snodePool.subtracting(guardSnodes)
let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount
guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them
return Set(guardSnodes.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 minSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
}
print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).")
return result
})
}
}
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
///
/// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: LokiAPITarget) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
// randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= pathCount {
return Promise<Path> { seal in
seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!)
}
} else {
return buildPaths().map(on: workQueue) { paths in
let path = paths.filter { !$0.contains(snode) }.randomElement()!
OnionRequestAPI.paths = paths
return path
}
}
}
private static func dropPath(containing snode: LokiAPITarget) {
paths = paths.filter { !$0.contains(snode) }
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt snode: LokiAPITarget) -> Promise<OnionBuildingResult> {
var guardSnode: LokiAPITarget!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
var encryptionResult: EncryptionResult!
return getPath(excluding: snode).then(on: workQueue) { path -> Promise<EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the target snode first
return encrypt(payload, forTargetSnode: snode).then(on: workQueue) { r -> Promise<EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = snode
func addLayer() -> Promise<EncryptionResult> {
if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
let lhs = path.removeLast()
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then(on: workQueue) { r -> Promise<EncryptionResult> in
encryptionResult = r
rhs = lhs
return addLayer()
}
}
}
return addLayer()
}
}.map(on: workQueue) { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
}
// MARK: Internal API
/// Sends an onion request to `snode`. Builds new paths as needed.
internal static func sendOnionRequest(invoking method: LokiAPITarget.Method, on snode: LokiAPITarget, with parameters: JSON, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
let (promise, seal) = Promise<JSON>.pending()
var guardSnode: LokiAPITarget!
workQueue.async {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ]
buildOnion(around: payload, targetedAt: snode).done(on: workQueue) { intermediate in
guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req"
let finalEncryptionResult = intermediate.finalEncryptionResult
let onion = finalEncryptionResult.ciphertext
let parameters: JSON = [
"ciphertext" : onion.base64EncodedString(),
"ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString()
]
let targetSnodeSymmetricKey = intermediate.targetSnodeSymmetricKey
HTTP.execute(.post, url, parameters: parameters).done(on: workQueue) { rawResponse in
guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String,
let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext) else { return seal.reject(HTTP.Error.invalidJSON) }
let iv = ivAndCiphertext[0..<Int(ivSize)]
let ciphertext = ivAndCiphertext[Int(ivSize)...]
do {
let gcm = GCM(iv: iv.bytes, tagLength: Int(gcmTagSize), mode: .combined)
let aes = try AES(key: targetSnodeSymmetricKey.bytes, blockMode: gcm, padding: .noPadding)
let data = Data(try aes.decrypt(ciphertext.bytes))
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? JSON,
let bodyAsString = json["body"] as? String, let bodyAsData = bodyAsString.data(using: .utf8),
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: []) as? JSON,
let statusCode = json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) }
guard 200...299 ~= statusCode else { return seal.reject(Error.httpRequestFailedAtTargetSnode(statusCode: UInt(statusCode), json: body)) }
seal.fulfill(body)
} catch (let error) {
seal.reject(error)
}
}.catch(on: workQueue) { error in
seal.reject(error)
}
}.catch(on: workQueue) { error in
seal.reject(error)
}
}
promise.catch(on: workQueue) { error in // Must be invoked on workQueue
guard case HTTP.Error.httpRequestFailed(_, _) = error else { return }
dropPath(containing: guardSnode) // A snode in the path is bad; retry with a different path
}
promise.handlingErrorsIfNeeded(forTargetSnode: snode, associatedWith: hexEncodedPublicKey)
return promise
}
}
// MARK: Target Snode Error Handling
private extension Promise where T == JSON {
func handlingErrorsIfNeeded(forTargetSnode snode: LokiAPITarget, associatedWith hexEncodedPublicKey: String) -> Promise<JSON> {
return recover(on: LokiAPI.errorHandlingQueue) { error -> Promise<JSON> in // Must be invoked on LokiAPI.errorHandlingQueue
// The code below is very similar to that in LokiAPI.handlingSnodeErrorsIfNeeded(for:associatedWith:), but unfortunately slightly
// different due to the fact that OnionRequestAPI uses the newer HTTP API, whereas LokiAPI still uses TSNetworkManager
guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error }
switch statusCode {
case 0, 400, 500, 503:
// The snode is unreachable
let oldFailureCount = LokiAPI.failureCount[snode] ?? 0
let newFailureCount = oldFailureCount + 1
LokiAPI.failureCount[snode] = newFailureCount
print("[Loki] Couldn't reach snode at: \(snode); setting failure count to \(newFailureCount).")
if newFailureCount >= LokiAPI.failureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
LokiAPI.dropIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey) // Remove it from the swarm cache associated with the given public key
LokiAPI.randomSnodePool.remove(snode) // Remove it from the random snode pool
LokiAPI.failureCount[snode] = 0
}
case 406:
print("[Loki] The user's clock is out of sync with the service node network.")
throw LokiAPI.LokiAPIError.clockOutOfSync
case 421:
// The snode isn't associated with the given public key anymore
print("[Loki] Invalidating swarm for: \(hexEncodedPublicKey).")
LokiAPI.dropIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey)
case 432:
// The proof of work difficulty is too low
if let powDifficulty = json["difficulty"] as? Int {
print("[Loki] Setting proof of work difficulty to \(powDifficulty).")
LokiAPI.powDifficulty = UInt(powDifficulty)
} else {
print("[Loki] Failed to update proof of work difficulty.")
}
break
default: break
}
throw error
}
}
}

@ -0,0 +1,53 @@
import CryptoSwift
import PromiseKit
@testable import SignalServiceKit
import XCTest
class OnionRequestAPITests : XCTestCase {
func testOnionRequestSending() {
let semaphore = DispatchSemaphore(value: 0)
var totalSuccessRate: Double = 0
let testCount = 10
LokiAPI.getRandomSnode().then(on: OnionRequestAPI.workQueue) { snode -> Promise<LokiAPITarget> in
print("[Loki] [Onion Request API] Target snode: \(snode).")
return OnionRequestAPI.getPath(excluding: snode).map(on: OnionRequestAPI.workQueue) { _ in snode } // Ensure we only build a path once
}.done(on: OnionRequestAPI.workQueue) { snode in
var successCount = 0
let promises: [Promise<Void>] = (0..<testCount).map { _ in
let mockSessionID = "0582bc30f11e8a9736407adcaca03b049f4acd4af3ae7eb6b6608d30f0b1e6a20e"
let parameters: JSON = [ "pubKey" : mockSessionID ]
let (promise, seal) = Promise<Void>.pending()
OnionRequestAPI.sendOnionRequest(invoking: .getSwarm, on: snode, with: parameters, associatedWith: mockSessionID).done(on: OnionRequestAPI.workQueue) { json in
successCount += 1
print("[Loki] [Onion Request API] Onion request succeeded with result: \(json.prettifiedDescription).")
seal.fulfill(())
}.catch(on: OnionRequestAPI.workQueue) { error in
if case GCM.Error.fail = error {
print("[Loki] [Onion Request API] Onion request failed due to a decryption error.")
} else {
print("[Loki] [Onion Request API] Onion request failed due to error: \(error).")
}
seal.reject(error)
}.finally(on: OnionRequestAPI.workQueue) {
let currentSuccessRate = min((100 * Double(successCount)) / Double(testCount), 100)
print("[Loki] [Onion Request API] Current onion request success rate: \(String(format: "%.1f", currentSuccessRate))%.")
}
return promise
}
when(resolved: promises).done(on: OnionRequestAPI.workQueue) { _ in
totalSuccessRate = min((100 * Double(successCount)) / Double(testCount), 100)
semaphore.signal()
}
}.catch(on: OnionRequestAPI.workQueue) { error in
print("[Loki] [Onion Request API] Path building failed due to error: \(error).")
semaphore.signal()
}
semaphore.wait()
print("[Loki] [Onion Request API] Total onion request success rate: \(String(format: "%.1f", totalSuccessRate))%.")
XCTAssert(totalSuccessRate >= 90)
}
// TODO: Test error handling
// TODO: Test race condition handling
}

@ -16,7 +16,7 @@ public class LokiSessionResetImplementation : NSObject, SessionResetProtocol {
public func validatePreKeyForFriendRequestAcceptance(for recipientID: String, whisperMessage: CipherMessage, protocolContext: Any?) throws {
guard let transaction = protocolContext as? YapDatabaseReadWriteTransaction else {
print("[Loki] Could not verify friend request acceptance pre key because an invalid transaction was provided.")
print("[Loki] Couldn't verify friend request acceptance pre key because an invalid transaction was provided.")
return
}
guard let preKeyMessage = whisperMessage as? PreKeyWhisperMessage else { return }
@ -32,7 +32,7 @@ public class LokiSessionResetImplementation : NSObject, SessionResetProtocol {
public func getSessionResetStatus(for recipientID: String, protocolContext: Any?) -> SessionResetStatus {
guard let transaction = protocolContext as? YapDatabaseReadWriteTransaction else {
print("[Loki] Could not get session reset status for \(recipientID) because an invalid transaction was provided.")
print("[Loki] Couldn't get session reset status for \(recipientID) because an invalid transaction was provided.")
return .none
}
guard let thread = TSContactThread.getWithContactId(recipientID, transaction: transaction) else { return .none }

@ -11,6 +11,7 @@
#import "YapDatabaseTransaction+OWS.h"
#import <AxolotlKit/NSData+keyVersionByte.h>
#import "NSObject+Casting.h"
#import <SignalServiceKit/SignalServiceKit-Swift.h>
@implementation OWSPrimaryStorage (Loki)
@ -58,7 +59,7 @@
@try {
return [self throws_loadPreKey:preKeyId];
} @catch (NSException *exception) {
NSLog(@"[Loki] New pre key generated for %@.", pubKey);
[LKLogger print:[NSString stringWithFormat:@"[Loki] New pre key generated for %@.", pubKey]];
return [self generateAndStorePreKeyForContact:pubKey];
}
}
@ -95,7 +96,7 @@
[signedPreKeyRecord markAsAcceptedByService];
[self storeSignedPreKey:signedPreKeyRecord.Id signedPreKeyRecord:signedPreKeyRecord];
[self setCurrentSignedPrekeyId:signedPreKeyRecord.Id];
NSLog(@"[Loki] Pre keys refreshed successfully.");
[LKLogger print:@"[Loki] Pre keys refreshed successfully."];
}
SignedPreKeyRecord *_Nullable signedPreKey = self.currentSignedPreKey;
@ -134,7 +135,7 @@
forceClean = YES;
}
}
OWSLogWarn(@"[Loki] Failed to generate a valid pre key bundle for: %@.", pubKey);
[LKLogger print:[NSString stringWithFormat:@"[Loki] Failed to generate a valid pre key bundle for: %@.", pubKey]];
return nil;
}

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

@ -27,7 +27,7 @@ final class LokiPushNotificationManager : NSObject {
return print("[Loki] Device token hasn't changed; no need to re-upload.")
}
guard !isUsingFullAPNs else {
return print("[Loki] Using full APNs; no need to upload device token.")
return print("[Loki] Using full APNs; ignoring call to register(with:).")
}
let parameters = [ "token" : hexEncodedToken ]
let url = URL(string: server + "register")!

@ -450,7 +450,7 @@ static const NSUInteger OWSMessageSchemaVersion = 4;
- (void)saveFriendRequestStatus:(LKMessageFriendRequestStatus)friendRequestStatus withTransaction:(YapDatabaseReadWriteTransaction *_Nullable)transaction
{
self.friendRequestStatus = friendRequestStatus;
NSLog(@"[Loki] Setting message friend request status to %@.", self.friendRequestStatusDescription);
[LKLogger print:[NSString stringWithFormat:@"[Loki] Setting message friend request status to %@.", self.friendRequestStatusDescription]];
void (^postNotification)() = ^() {
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.messageFriendRequestStatusChanged object:self.uniqueId];
};

@ -450,7 +450,7 @@ NS_ASSUME_NONNULL_BEGIN
// Loki: Handle pre key bundle message if needed
if (contentProto.prekeyBundleMessage != nil) {
OWSLogInfo(@"[Loki] Received a pre key bundle message from: %@.", envelope.source);
[LKLogger print:[NSString stringWithFormat:@"[Loki] Received a pre key bundle message from: %@.", envelope.source]];
PreKeyBundle *_Nullable bundle = [contentProto.prekeyBundleMessage getPreKeyBundleWithTransaction:transaction];
if (bundle == nil) {
OWSFailDebug(@"Failed to create a pre key bundle.");
@ -484,7 +484,7 @@ NS_ASSUME_NONNULL_BEGIN
NSData *masterSignature = contentProto.lokiDeviceLinkMessage.masterSignature;
NSData *slaveSignature = contentProto.lokiDeviceLinkMessage.slaveSignature;
if (masterSignature != nil) { // Authorization
OWSLogInfo(@"[Loki] Received a device linking authorization from: %@", envelope.source); // Not masterHexEncodedPublicKey
[LKLogger print:[NSString stringWithFormat:@"[Loki] Received a device linking authorization from: %@", envelope.source]]; // Not masterHexEncodedPublicKey
[LKDeviceLinkingSession.current processLinkingAuthorizationFrom:masterHexEncodedPublicKey for:slaveHexEncodedPublicKey masterSignature:masterSignature slaveSignature:slaveSignature];
// Set any profile info
if (contentProto.dataMessage) {
@ -493,7 +493,7 @@ NS_ASSUME_NONNULL_BEGIN
[self handleProfileKeyUpdateIfNeeded:dataMessage recipientId:masterHexEncodedPublicKey];
}
} else if (slaveSignature != nil) { // Request
OWSLogInfo(@"[Loki] Received a device linking request from: %@", envelope.source); // Not slaveHexEncodedPublicKey
[LKLogger print: [NSString stringWithFormat:@"[Loki] Received a device linking request from: %@", envelope.source]]; // Not slaveHexEncodedPublicKey
if (LKDeviceLinkingSession.current == nil) {
[NSNotificationCenter.defaultCenter postNotificationName:NSNotification.unexpectedDeviceLinkRequestReceived object:nil];
}
@ -1140,7 +1140,7 @@ NS_ASSUME_NONNULL_BEGIN
}
} else if (syncMessage.openGroups != nil) {
if (wasSentByMasterDevice && syncMessage.openGroups.count > 0) {
OWSLogInfo(@"[Loki] Received open group sync message.");
[LKLogger print:@"[Loki] Received open group sync message."];
for (SSKProtoSyncMessageOpenGroups* openGroup in syncMessage.openGroups) {
[LKPublicChatManager.shared addChatWithServer:openGroup.url channel:openGroup.channel];
}
@ -1191,7 +1191,7 @@ NS_ASSUME_NONNULL_BEGIN
LKEphemeralMessage *emptyMessage = [[LKEphemeralMessage alloc] initInThread:thread];
[self.messageSenderJobQueue addMessage:emptyMessage transaction:transaction];
NSLog(@"[Loki] Session reset received from %@.", hexEncodedPublicKey);
[LKLogger print:[NSString stringWithFormat:@"[Loki] Session reset received from %@.", hexEncodedPublicKey]];
}
- (void)handleExpirationTimerUpdateMessageWithEnvelope:(SSKProtoEnvelope *)envelope
@ -1788,11 +1788,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)handleFriendRequestMessageIfNeededWithEnvelope:(SSKProtoEnvelope *)envelope data:(SSKProtoDataMessage *)data message:(TSIncomingMessage *)message thread:(TSContactThread *)thread transaction:(YapDatabaseReadWriteTransaction *)transaction {
if (envelope.isGroupChatMessage) {
return NSLog(@"[Loki] Ignoring friend request in group chat.", @"");
return [LKLogger print:@"[Loki] Ignoring friend request in group chat."];
}
// The envelope type is set during UD decryption.
if (envelope.type != SSKProtoEnvelopeTypeFriendRequest) {
return NSLog(@"[Loki] Ignoring friend request logic for non friend request type envelope.");
return [LKLogger print:@"[Loki] Ignoring friend request logic for non friend request type envelope."];
}
if ([self canFriendRequestBeAutoAcceptedForThread:thread transaction:transaction]) {
[thread saveFriendRequestStatus:LKThreadFriendRequestStatusFriends withTransaction:transaction];

@ -39,6 +39,7 @@ class OWSUDManagerTest: SSKBaseTestSwift {
// MARK: registration
let aliceRecipientId = "+13213214321"
/*
override func setUp() {
super.setUp()
@ -61,6 +62,7 @@ class OWSUDManagerTest: SSKBaseTestSwift {
udManager.setSenderCertificate(try! senderCertificate.serialized())
}
*/
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.

@ -47,7 +47,7 @@
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>imaginary.stream</key>
<key>public.loki.foundation</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>

Loading…
Cancel
Save