From 50b41c63aff5e17b3ab5b97e3a00d2603ef64165 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 21 May 2019 13:06:46 +1000 Subject: [PATCH 01/10] Added storing of last message hash. --- Pods | 2 +- SignalServiceKit/src/Loki/API/LokiAPI.swift | 23 ++++++++++++ .../src/Loki/Crypto/OWSPrimaryStorage+Loki.h | 28 +++++++++++++- .../src/Loki/Crypto/OWSPrimaryStorage+Loki.m | 37 +++++++++++++++++++ 4 files changed, 87 insertions(+), 3 deletions(-) diff --git a/Pods b/Pods index e5b45b28d..c1bf4bf2f 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit e5b45b28d5e8e409c1acf15c32d3734bb3e8acd7 +Subproject commit c1bf4bf2fb27eaacb36e60b17d30f92f626d4368 diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 748fcf45d..2618838e3 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -102,3 +102,26 @@ private extension Promise { } } } + +// MARK: Last Hash + +extension LokiAPI { + + private var primaryStorage: OWSPrimaryStorage { + return OWSPrimaryStorage.shared() + } + + fileprivate func updateLastHash(for node: Target, hash: String, expiresAt: UInt64) { + primaryStorage.dbReadWriteConnection.readWrite { transaction in + self.primaryStorage.setLastMessageHash(hash, expiresAt: expiresAt, serviceNode: node.address, transaction: transaction) + } + } + + fileprivate func getLastHash(for node: Target) -> String? { + var lastHash: String? = nil + primaryStorage.dbReadWriteConnection.readWrite { transaction in + lastHash = self.primaryStorage.getLastMessageHash(forServiceNode: node.address, transaction: transaction) + } + return lastHash + } +} diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 9a9a694b4..8f97d5e05 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -1,8 +1,9 @@ #import "OWSPrimaryStorage.h" -#import "PreKeyRecord.h" -#import "PreKeyBundle.h" #import +@class PreKeyRecord; +@class PreKeyBundle; + NS_ASSUME_NONNULL_BEGIN @interface OWSPrimaryStorage (Loki) @@ -72,6 +73,29 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction; +# pragma mark - Last Hash + +/** + Get the last message hash for the given service node. + This function will check the stored last hash and remove it if the `expireAt` has already passed. + + @param serviceNode The service node id + @param transaction A read write transaction + @return The last hash or nil if it doesn't exist + */ +- (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction; + +/** + Set the last message hash for the given service node. + This will override any previous hashes stored for the given service node. + + @param hash The last message hash + @param expiresAt The time the message expires on the server + @param serviceNode The service node + @param transaction A read write transaction + */ +- (void)setLastMessageHash:(NSString *)hash expiresAt:(u_int64_t)expiresAt serviceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index 125679a53..1b678d205 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -4,6 +4,9 @@ #import "OWSPrimaryStorage+keyFromIntLong.h" #import "OWSDevice.h" #import "OWSIdentityManager.h" +#import "NSDate+OWS.h" +#import "PreKeyRecord.h" +#import "PreKeyBundle.h" #import "TSAccountManager.h" #import "TSPreKeyManager.h" #import "YapDatabaseConnection+OWS.h" @@ -13,6 +16,7 @@ #define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" #define LokiPreKeyContactCollection @"LokiPreKeyContactCollection" #define LokiPreKeyBundleCollection @"LokiPreKeyBundleCollection" +#define LokiLastHashCollection @"LokiLastHashCollection" @implementation OWSPrimaryStorage (Loki) @@ -118,4 +122,37 @@ [transaction removeObjectForKey:pubKey inCollection:LokiPreKeyBundleCollection]; } +# pragma mark - Last Hash + +- (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LokiLastHashCollection]; + if (!dict) { return nil; } + + NSString *_Nullable hash = dict[@"hash"]; + if (!hash) { return nil; } + + // Check if the hash isn't expired + uint64_t now = NSDate.ows_millisecondTimeStamp; + NSNumber *_Nullable expiresAt = dict[@"expiresAt"]; + if (expiresAt && expiresAt.unsignedLongLongValue <= now) { + // The last message has expired from the storage server + [self removeLastMessageHashForServiceNode:serviceNode transaction:transaction]; + return nil; + } + + return hash; +} + +- (void)setLastMessageHash:(NSString *)hash expiresAt:(u_int64_t)expiresAt serviceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + NSDictionary *dict = @{ + @"hash": hash, + @"expiresAt": @(expiresAt) + }; + [transaction setObject:dict forKey:serviceNode inCollection:LokiLastHashCollection]; +} + +- (void)removeLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction removeObjectForKey:serviceNode inCollection:LokiLastHashCollection]; +} + @end From 9d7cec90e1012a5273f33cd2710f5f5b3169f783 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 21 May 2019 13:26:51 +1000 Subject: [PATCH 02/10] WIP --- SignalServiceKit/src/Loki/API/LokiAPI.swift | 82 ++++++++++++--------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 748fcf45d..84da0a506 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -3,18 +3,19 @@ import PromiseKit @objc public final class LokiAPI : NSObject { private static let version = "v1" + private static let targetSnodeCount = 2 public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 // MARK: Types - private enum Method : String { - case getMessages = "retrieve" - case sendMessage = "store" - case getSwarm = "get_snodes_for_pubkey" - } - - public struct Target : Hashable { + private struct Target : Hashable { let address: String let port: UInt16 + + enum Method : String { + case getSwarm = "get_snodes_for_pubkey" + case getMessages = "retrieve" + case sendMessage = "store" + } } public typealias RawResponse = Any @@ -32,37 +33,36 @@ import PromiseKit // MARK: Lifecycle override private init() { } - // MARK: API - private static func invoke(_ method: Method, on target: Target, with parameters: [String:Any] = [:]) -> Promise { + // MARK: Internal API + private static func invoke(_ method: Target.Method, on target: Target, with parameters: [String:Any] = [:]) -> Promise { let url = URL(string: "\(target.address):\(target.port)/\(version)/storage_rpc")! let request = TSRequest(url: url, method: "POST", parameters: [ "method" : method.rawValue, "params" : parameters ]) return TSNetworkManager.shared().makePromise(request: request).map { $0.responseObject } } - public static func getRandomSnode() -> Promise { + private static func getRandomSnode() -> Promise { return Promise { seal in seal.fulfill(Target(address: "http://13.238.53.205", port: 8080)) // TODO: Temporary } } - public static func getMessages() -> Promise<[SSKProtoEnvelope]> { - let parameters = [ - "pubKey" : OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey, - "lastHash" : "" // TODO: Implement - ] - return getRandomSnode().then { invoke(.getMessages, on: $0, with: parameters) }.map { rawResponse in // TODO: Use getSwarm() - guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } - return messages.compactMap { message in - guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { - Logger.warn("[Loki] Failed to decode data for message: \(message).") - return nil - } - guard let envelope = try? unwrap(data: data) else { - Logger.warn("[Loki] Failed to unwrap data for message: \(message).") - return nil - } - return envelope - } + private static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in return [] } // TODO: Parse targets from raw response + } + + private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise> { + return getSwarm(for: hexEncodedPublicKey).map { Set(Array($0).shuffled().prefix(targetSnodeCount)) } + } + + // MARK: Public API + public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> + + public static func getMessages() -> Promise<[MessagesPromise]> { + let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + let lastHash = "" // TODO: Implement + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : "" ] + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in + return invoke(.getMessages, on: targetSnode, with: parameters).map { parseProtoEnvelopes(from: $0) } } } @@ -74,19 +74,29 @@ import PromiseKit return getRandomSnode().then { invoke(.sendMessage, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) } // TODO: Use getSwarm() and figure out correct parameters } - public static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { - return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in return [] } // TODO: Parse targets from raw response - } - - // MARK: Obj-C API + // MARK: Public API (Obj-C) @objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> AnyPromise { - let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired) - .then(sendMessage) - .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) + let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired).then(sendMessage).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) let anyPromise = AnyPromise(promise) anyPromise.retainUntilComplete() return anyPromise } + + // MARK: Convenience + private static func parseProtoEnvelopes(from rawResponse: Any) -> [SSKProtoEnvelope] { + guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } + return messages.compactMap { message in + guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { + Logger.warn("[Loki] Failed to decode data for message: \(message).") + return nil + } + guard let envelope = try? unwrap(data: data) else { + Logger.warn("[Loki] Failed to unwrap data for message: \(message).") + return nil + } + return envelope + } + } } // MARK: - Convenience From 59de49641ed2b450347643bbdc9e184bb19e6715 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 21 May 2019 13:44:46 +1000 Subject: [PATCH 03/10] Implement swarm caching --- SignalServiceKit/src/Loki/API/LokiAPI.swift | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 84da0a506..d9c176064 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -2,6 +2,10 @@ import PromiseKit @objc public final class LokiAPI : NSObject { + // MARK: Caching + private static var swarmCache: [String:Set] = [:] + + // MARK: Settings private static let version = "v1" private static let targetSnodeCount = 2 public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 @@ -30,6 +34,8 @@ import PromiseKit } } + public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> + // MARK: Lifecycle override private init() { } @@ -47,7 +53,13 @@ import PromiseKit } private static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { - return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in return [] } // TODO: Parse targets from raw response + if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount { + return Promise> { $0.fulfill(cachedSwarm) } + } else { + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in + return [] // TODO: Parse targets from raw response + }.get { swarmCache[hexEncodedPublicKey] = $0 } + } } private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise> { @@ -55,8 +67,6 @@ import PromiseKit } // MARK: Public API - public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> - public static func getMessages() -> Promise<[MessagesPromise]> { let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey let lastHash = "" // TODO: Implement @@ -99,12 +109,10 @@ import PromiseKit } } -// MARK: - Convenience - private extension Promise { func recoverNetworkErrorIfNeeded(on queue: DispatchQueue) -> Promise { - return self.recover(on: queue) { error -> Promise in + return recover(on: queue) { error -> Promise in switch error { case NetworkManagerError.taskError(_, let underlyingError): throw underlyingError default: throw error From a12d36fb9e5888597a589b403852b9a1a7395930 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 21 May 2019 13:48:42 +1000 Subject: [PATCH 04/10] Set last hash in params. --- SignalServiceKit/src/Loki/API/LokiAPI.swift | 22 +++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 3de2b5dc7..b371c5bc2 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -7,7 +7,7 @@ import PromiseKit public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 // MARK: Types - private struct Target : Hashable { + fileprivate struct Target : Hashable { let address: String let port: UInt16 @@ -59,10 +59,16 @@ import PromiseKit public static func getMessages() -> Promise<[MessagesPromise]> { let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey - let lastHash = "" // TODO: Implement - let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : "" ] return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in - return invoke(.getMessages, on: targetSnode, with: parameters).map { parseProtoEnvelopes(from: $0) } + let lastHash = getLastHash(for: targetSnode) ?? "" + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ] + return invoke(.getMessages, on: targetSnode, with: parameters).map { response in + if let json = response as? JSON, let messages = json["messages"] as? [JSON], let lastMessage = messages.last, + let hash = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int { + updateLastHash(for: targetSnode, hash: hash, expiresAt: UInt64(expiresAt)) + } + return parseProtoEnvelopes(from: response) + } } } @@ -115,19 +121,19 @@ private extension Promise { // MARK: Last Hash -extension LokiAPI { +fileprivate extension LokiAPI { - private var primaryStorage: OWSPrimaryStorage { + private static var primaryStorage: OWSPrimaryStorage { return OWSPrimaryStorage.shared() } - fileprivate func updateLastHash(for node: Target, hash: String, expiresAt: UInt64) { + fileprivate static func updateLastHash(for node: Target, hash: String, expiresAt: UInt64) { primaryStorage.dbReadWriteConnection.readWrite { transaction in self.primaryStorage.setLastMessageHash(hash, expiresAt: expiresAt, serviceNode: node.address, transaction: transaction) } } - fileprivate func getLastHash(for node: Target) -> String? { + fileprivate static func getLastHash(for node: Target) -> String? { var lastHash: String? = nil primaryStorage.dbReadWriteConnection.readWrite { transaction in lastHash = self.primaryStorage.getLastMessageHash(forServiceNode: node.address, transaction: transaction) From dc103c7bd617af19861562a623d006cc7fed1dbd Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 21 May 2019 15:21:51 +1000 Subject: [PATCH 05/10] Refactor --- Signal.xcodeproj/project.pbxproj | 12 ++-- Signal/src/Jobs/MessageFetcherJob.swift | 15 +++-- .../HomeView/HomeViewController.m | 2 +- .../translations/en.lproj/Localizable.strings | 2 +- SignalServiceKit/src/Loki/API/LokiAPI.swift | 64 +++++++++---------- .../src/Loki/Crypto/OWSPrimaryStorage+Loki.h | 18 +++--- .../src/Loki/Crypto/OWSPrimaryStorage+Loki.m | 2 +- 7 files changed, 56 insertions(+), 59 deletions(-) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c5a7426fb..9a8604f0a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1355,7 +1355,7 @@ D17BB5C25D615AB49813100C /* Pods_Signal.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Signal.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D2179CFB16BB0B3A0006F3AB /* CoreTelephony.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreTelephony.framework; path = System/Library/Frameworks/CoreTelephony.framework; sourceTree = SDKROOT; }; D2179CFD16BB0B480006F3AB /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; }; - D221A089169C9E5E00537ABF /* Session.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Session.app; sourceTree = BUILT_PRODUCTS_DIR; }; + D221A089169C9E5E00537ABF /* Signal.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Signal.app; sourceTree = BUILT_PRODUCTS_DIR; }; D221A08D169C9E5E00537ABF /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D221A08F169C9E5E00537ABF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; D221A091169C9E5E00537ABF /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -2625,7 +2625,7 @@ D221A08A169C9E5E00537ABF /* Products */ = { isa = PBXGroup; children = ( - D221A089169C9E5E00537ABF /* Session.app */, + D221A089169C9E5E00537ABF /* Signal.app */, D221A0AA169C9E5F00537ABF /* SignalTests.xctest */, 453518681FC635DD00210559 /* SignalShareExtension.appex */, 453518921FC63DBF00210559 /* SignalMessaging.framework */, @@ -2861,7 +2861,7 @@ ); name = Signal; productName = RedPhone; - productReference = D221A089169C9E5E00537ABF /* Session.app */; + productReference = D221A089169C9E5E00537ABF /* Signal.app */; productType = "com.apple.product-type.application"; }; D221A0A9169C9E5F00537ABF /* SignalTests */ = { @@ -4353,12 +4353,13 @@ LLVM_LTO = NO; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.niels-andriesse.loki-network.Loki-Messenger"; - PRODUCT_NAME = Session; + PRODUCT_NAME = Signal; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_BRIDGING_HEADER = "Signal/src/Signal-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TEST_AFTER_BUILD = YES; @@ -4418,11 +4419,12 @@ LLVM_LTO = NO; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.niels-andriesse.loki-network.Loki-Messenger"; - PRODUCT_NAME = Session; + PRODUCT_NAME = Signal; PROVISIONING_PROFILE = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; SWIFT_OBJC_BRIDGING_HEADER = "Signal/src/Signal-Bridging-Header.h"; + SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_VERSION = 5.0; TEST_AFTER_BUILD = YES; VALID_ARCHS = "arm64 armv7 armv7s"; diff --git a/Signal/src/Jobs/MessageFetcherJob.swift b/Signal/src/Jobs/MessageFetcherJob.swift index bc86d5638..d1e8da742 100644 --- a/Signal/src/Jobs/MessageFetcherJob.swift +++ b/Signal/src/Jobs/MessageFetcherJob.swift @@ -175,12 +175,13 @@ public class MessageFetcherJob: NSObject { } private func fetchUndeliveredMessages() -> Promise<(envelopes: [SSKProtoEnvelope], more: Bool)> { - return Promise { resolver in - LokiAPI.getMessages().done { envelopes in - resolver.fulfill((envelopes: envelopes, more: false)) - }.catch { error in - resolver.reject(error) - } + notImplemented() +// return Promise { resolver in +// LokiAPI.getMessages().done { envelopes in +// resolver.fulfill((envelopes: envelopes, more: false)) +// }.catch { error in +// resolver.reject(error) +// } // Loki: Original code // ======== // let request = OWSRequestFactory.getMessagesRequest() @@ -203,7 +204,7 @@ public class MessageFetcherJob: NSObject { // resolver.reject(error) // }) // ======== - } +// } } private func acknowledgeDelivery(envelope: SSKProtoEnvelope) { diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 1637ae1e2..6f8a794a9 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -619,7 +619,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) { switch (self.homeViewMode) { case HomeViewMode_Inbox: // TODO: Should our app name be translated? Probably not. - self.title = NSLocalizedString(@"Session", @"Title for the home view's default mode."); + self.title = NSLocalizedString(@"Signal", @"Title for the home view's default mode."); break; case HomeViewMode_Archive: self.title = NSLocalizedString(@"HOME_VIEW_TITLE_ARCHIVE", @"Title for the home view's 'archive' mode."); diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index dab2c54e4..5df4197b7 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2578,7 +2578,7 @@ "New Message" = "New Message"; "Secure session reset in progress" = "Secure session reset in progress"; "Secure session reset done" = "Secure session reset done"; -"Session" = "Session"; +"Signal" = "Signal"; "You've sent %@ a friend request" = "You've sent %@ a friend request"; "You've declined %@'s friend request" = "You've declined %@'s friend request"; "You've accepted %@'s friend request" = "You've accepted %@'s friend request"; diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 40806a1b1..d84d7fa6e 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -1,6 +1,7 @@ import PromiseKit @objc public final class LokiAPI : NSObject { + private static let storage = OWSPrimaryStorage.shared() // MARK: Caching private static var swarmCache: [String:Set] = [:] @@ -34,7 +35,7 @@ import PromiseKit } } - public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> + public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> // To keep the return type of getMessages() readable // MARK: Lifecycle override private init() { } @@ -47,18 +48,15 @@ import PromiseKit } private static func getRandomSnode() -> Promise { - return Promise { seal in - seal.fulfill(Target(address: "http://13.238.53.205", port: 8080)) // TODO: Temporary - } + return Promise { _ in notImplemented() } // TODO: Implement } private static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount { return Promise> { $0.fulfill(cachedSwarm) } } else { - return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) }.map { rawResponse in - return [] // TODO: Parse targets from raw response - }.get { swarmCache[hexEncodedPublicKey] = $0 } + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) } + .map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } } } @@ -72,12 +70,12 @@ import PromiseKit return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in let lastHash = getLastHash(for: targetSnode) ?? "" let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ] - return invoke(.getMessages, on: targetSnode, with: parameters).map { response in - if let json = response as? JSON, let messages = json["messages"] as? [JSON], let lastMessage = messages.last, + return invoke(.getMessages, on: targetSnode, with: parameters).map { rawResponse in + if let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON], let lastMessage = messages.last, let hash = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int { - updateLastHash(for: targetSnode, hash: hash, expiresAt: UInt64(expiresAt)) + setLastHash(for: targetSnode, hash: hash, expiresAt: UInt64(expiresAt)) } - return parseProtoEnvelopes(from: response) + return parseProtoEnvelopes(from: rawResponse) } } } @@ -98,7 +96,26 @@ import PromiseKit return anyPromise } - // MARK: Convenience + // MARK: Last Hash + private static func setLastHash(for target: Target, hash: String, expiresAt: UInt64) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setLastMessageHashForServiceNode(target.address, hash: hash, expiresAt: expiresAt, transaction: transaction) + } + } + + private static func getLastHash(for target: Target) -> String? { + var lastHash: String? + storage.dbReadWriteConnection.readWrite { transaction in + lastHash = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) + } + return lastHash + } + + // MARK: Parsing + private static func parseTargets(from rawResponse: Any) -> Set { + notImplemented() + } + private static func parseProtoEnvelopes(from rawResponse: Any) -> [SSKProtoEnvelope] { guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } return messages.compactMap { message in @@ -126,26 +143,3 @@ private extension Promise { } } } - -// MARK: Last Hash - -fileprivate extension LokiAPI { - - private static var primaryStorage: OWSPrimaryStorage { - return OWSPrimaryStorage.shared() - } - - fileprivate static func updateLastHash(for node: Target, hash: String, expiresAt: UInt64) { - primaryStorage.dbReadWriteConnection.readWrite { transaction in - self.primaryStorage.setLastMessageHash(hash, expiresAt: expiresAt, serviceNode: node.address, transaction: transaction) - } - } - - fileprivate static func getLastHash(for node: Target) -> String? { - var lastHash: String? = nil - primaryStorage.dbReadWriteConnection.readWrite { transaction in - lastHash = self.primaryStorage.getLastMessageHash(forServiceNode: node.address, transaction: transaction) - } - return lastHash - } -} diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 8f97d5e05..bf0378a3b 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -77,11 +77,11 @@ NS_ASSUME_NONNULL_BEGIN /** Get the last message hash for the given service node. - This function will check the stored last hash and remove it if the `expireAt` has already passed. + This function will check the stored last hash and remove it if the `expiresAt` has already passed. - @param serviceNode The service node id - @param transaction A read write transaction - @return The last hash or nil if it doesn't exist + @param serviceNode The service node ID. + @param transaction A read write transaction. + @return The last hash or `nil` if it doesn't exist. */ - (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction; @@ -89,12 +89,12 @@ NS_ASSUME_NONNULL_BEGIN Set the last message hash for the given service node. This will override any previous hashes stored for the given service node. - @param hash The last message hash - @param expiresAt The time the message expires on the server - @param serviceNode The service node - @param transaction A read write transaction + @param serviceNode The service node ID. + @param hash The last message hash. + @param expiresAt The time the message expires on the server. + @param transaction A read write transaction. */ -- (void)setLastMessageHash:(NSString *)hash expiresAt:(u_int64_t)expiresAt serviceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction; @end diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index 1b678d205..70ee07f30 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -143,7 +143,7 @@ return hash; } -- (void)setLastMessageHash:(NSString *)hash expiresAt:(u_int64_t)expiresAt serviceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { +- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction { NSDictionary *dict = @{ @"hash": hash, @"expiresAt": @(expiresAt) From 167c1547966b5aca944494757f51fac1be2d0fe8 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 21 May 2019 15:47:19 +1000 Subject: [PATCH 06/10] Fix up imports. --- SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h | 5 ++--- SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index bf0378a3b..39e5fe5df 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -1,9 +1,8 @@ #import "OWSPrimaryStorage.h" +#import +#import #import -@class PreKeyRecord; -@class PreKeyBundle; - NS_ASSUME_NONNULL_BEGIN @interface OWSPrimaryStorage (Loki) diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index 70ee07f30..ff57f5b35 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -5,8 +5,6 @@ #import "OWSDevice.h" #import "OWSIdentityManager.h" #import "NSDate+OWS.h" -#import "PreKeyRecord.h" -#import "PreKeyBundle.h" #import "TSAccountManager.h" #import "TSPreKeyManager.h" #import "YapDatabaseConnection+OWS.h" From b1e18de5e230cddd29cabd47cde6c879522c87a8 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 21 May 2019 16:40:31 +1000 Subject: [PATCH 07/10] Implement swarm parsing --- SignalServiceKit/src/Loki/API/LokiAPI.swift | 32 +++++++++++-------- .../src/Loki/Utilities/Promise+Hashable.swift | 13 ++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index d84d7fa6e..bb7b07536 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -8,11 +8,12 @@ import PromiseKit // MARK: Settings private static let version = "v1" + private static let defaultSnodePort: UInt16 = 8080 private static let targetSnodeCount = 2 public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 // MARK: Types - fileprivate struct Target : Hashable { + private struct Target : Hashable { let address: String let port: UInt16 @@ -35,8 +36,6 @@ import PromiseKit } } - public typealias MessagesPromise = Promise<[SSKProtoEnvelope]> // To keep the return type of getMessages() readable - // MARK: Lifecycle override private init() { } @@ -65,7 +64,7 @@ import PromiseKit } // MARK: Public API - public static func getMessages() -> Promise<[MessagesPromise]> { + public static func getMessages() -> Promise>>> { let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in let lastHash = getLastHash(for: targetSnode) ?? "" @@ -77,20 +76,24 @@ import PromiseKit } return parseProtoEnvelopes(from: rawResponse) } - } + }.map { Set($0) } } - public static func sendMessage(_ lokiMessage: Message) -> Promise { - return getRandomSnode().then { invoke(.sendMessage, on: $0, with: lokiMessage.toJSON()) } // TODO: Use getSwarm() + public static func sendMessage(_ lokiMessage: Message) -> Promise<[Promise]> { + let parameters = lokiMessage.toJSON() + return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters) + .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) } } - public static func ping(_ hexEncodedPublicKey: String) -> Promise { - return getRandomSnode().then { invoke(.sendMessage, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) } // TODO: Use getSwarm() and figure out correct parameters + public static func ping(_ hexEncodedPublicKey: String) -> Promise<[Promise]> { + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] // TODO: Figure out correct parameters + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters) + .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) } } // MARK: Public API (Obj-C) @objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> AnyPromise { - let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired).then(sendMessage).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) + let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired).then(sendMessage) let anyPromise = AnyPromise(promise) anyPromise.retainUntilComplete() return anyPromise @@ -113,12 +116,13 @@ import PromiseKit // MARK: Parsing private static func parseTargets(from rawResponse: Any) -> Set { - notImplemented() + guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else { return [] } + return Set(addresses.map { Target(address: $0, port: defaultSnodePort) }) } - private static func parseProtoEnvelopes(from rawResponse: Any) -> [SSKProtoEnvelope] { + private static func parseProtoEnvelopes(from rawResponse: Any) -> Set { guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } - return messages.compactMap { message in + return Set(messages.compactMap { message in guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { Logger.warn("[Loki] Failed to decode data for message: \(message).") return nil @@ -128,7 +132,7 @@ import PromiseKit return nil } return envelope - } + }) } } diff --git a/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift b/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift new file mode 100644 index 000000000..0d2ec3bd7 --- /dev/null +++ b/SignalServiceKit/src/Loki/Utilities/Promise+Hashable.swift @@ -0,0 +1,13 @@ +import PromiseKit + +extension Promise : Hashable { + + public func hash(into hasher: inout Hasher) { + let reference = ObjectIdentifier(self).hashValue + hasher.combine(reference) + } + + public static func == (lhs: Promise, rhs: Promise) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} From 4d2e299d6d5e99211ef41240fdd397a201b1057e Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Tue, 21 May 2019 16:41:41 +1000 Subject: [PATCH 08/10] Update Pods --- Pods | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Pods b/Pods index c1bf4bf2f..44417f949 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit c1bf4bf2fb27eaacb36e60b17d30f92f626d4368 +Subproject commit 44417f9493b8c89b45c1acb72e538d7cd773ebd7 From f3e27370439aad9cfe00cf87c5d279f8216b784f Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 22 May 2019 09:36:35 +1000 Subject: [PATCH 09/10] Fix inconsistent function signatures --- SignalServiceKit/src/Loki/API/LokiAPI.swift | 2 +- SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index bb7b07536..604e7dce9 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -102,7 +102,7 @@ import PromiseKit // MARK: Last Hash private static func setLastHash(for target: Target, hash: String, expiresAt: UInt64) { storage.dbReadWriteConnection.readWrite { transaction in - storage.setLastMessageHashForServiceNode(target.address, hash: hash, expiresAt: expiresAt, transaction: transaction) + storage.setLastMessageHash(forServiceNode: target.address, hash: hash, expiresAt: expiresAt, transaction: transaction) } } diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 39e5fe5df..59a552744 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -93,7 +93,7 @@ NS_ASSUME_NONNULL_BEGIN @param expiresAt The time the message expires on the server. @param transaction A read write transaction. */ -- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction; +- (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forServiceNode:hash:expiresAt:transaction:)); @end From e5463e545ac6be6c13b9d3cf65d9105ad475b4dc Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 22 May 2019 11:32:32 +1000 Subject: [PATCH 10/10] Filter duplicate messages & make parsing strategy consistent --- .../xcschemes/Signal-Internal.xcscheme | 8 +- .../xcshareddata/xcschemes/Signal.xcscheme | 8 +- .../xcschemes/SignalShareExtension.xcscheme | 6 +- Signal/Signal-Info.plist | 2 +- .../src/Loki/API/LokiAPI+Wrapping.swift | 32 ++--- SignalServiceKit/src/Loki/API/LokiAPI.swift | 125 ++++++++++++------ .../src/Loki/Crypto/OWSPrimaryStorage+Loki.h | 5 +- .../src/Loki/Crypto/OWSPrimaryStorage+Loki.m | 42 +++--- 8 files changed, 140 insertions(+), 88 deletions(-) diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme index c5583e4d9..572fd60ab 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal-Internal.xcscheme @@ -15,7 +15,7 @@ @@ -33,7 +33,7 @@ @@ -56,7 +56,7 @@ @@ -87,7 +87,7 @@ diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme index 8c3c2011a..e523f2c19 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/Signal.xcscheme @@ -15,7 +15,7 @@ @@ -117,7 +117,7 @@ @@ -152,7 +152,7 @@ @@ -193,7 +193,7 @@ diff --git a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme index 77ad0a66b..cc64d8a0e 100644 --- a/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme +++ b/Signal.xcodeproj/xcshareddata/xcschemes/SignalShareExtension.xcscheme @@ -30,7 +30,7 @@ @@ -72,7 +72,7 @@ @@ -92,7 +92,7 @@ diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist index 7396df8e5..258a90a89 100644 --- a/Signal/Signal-Info.plist +++ b/Signal/Signal-Info.plist @@ -7,7 +7,7 @@ CarthageVersion 0.33.0 OSXVersion - 10.14.4 + 10.14.5 WebRTCCommit 1445d719bf05280270e9f77576f80f973fd847f8 M73 diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift index 2a47b47e2..255a5652e 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+Wrapping.swift @@ -35,22 +35,6 @@ extension LokiAPI { } } - /// Unwrap data sent by the storage server. - /// - /// - Parameter data: The data from the storage server (not base 64 encoded). - /// - Returns: An `SSKProtoEnvelope` object. - /// - Throws: A `WrappingError` if something went wrong. - static func unwrap(data: Data) throws -> SSKProtoEnvelope { - do { - let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) - let envelope = webSocketMessage.request!.body! - return try SSKProtoEnvelope.parseData(envelope) - } catch let error { - owsFailDebug("[Loki] Failed to unwrap data: \(error).") - throw WrappingError.failedToUnwrapData - } - } - /// Wrap an `SSKProtoEnvelope` in a `WebSocketProtoWebSocketMessage`. private static func createWebSocketMessage(around envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage { do { @@ -91,4 +75,20 @@ extension LokiAPI { throw WrappingError.failedToWrapMessageInEnvelope } } + + /// Unwrap data sent by the storage server. + /// + /// - Parameter data: The data from the storage server (not base 64 encoded). + /// - Returns: An `SSKProtoEnvelope` object. + /// - Throws: A `WrappingError` if something went wrong. + static func unwrap(data: Data) throws -> SSKProtoEnvelope { + do { + let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) + let envelope = webSocketMessage.request!.body! + return try SSKProtoEnvelope.parseData(envelope) + } catch let error { + owsFailDebug("[Loki] Failed to unwrap data: \(error).") + throw WrappingError.failedToUnwrapData + } + } } diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index 604e7dce9..a8e5463cf 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -3,15 +3,15 @@ import PromiseKit @objc public final class LokiAPI : NSObject { private static let storage = OWSPrimaryStorage.shared() - // MARK: Caching - private static var swarmCache: [String:Set] = [:] - // MARK: Settings private static let version = "v1" private static let defaultSnodePort: UInt16 = 8080 private static let targetSnodeCount = 2 public static let defaultMessageTTL: UInt64 = 4 * 24 * 60 * 60 + // MARK: Caching + private static var swarmCache: [String:[Target]] = [:] + // MARK: Types private struct Target : Hashable { let address: String @@ -50,45 +50,43 @@ import PromiseKit return Promise { _ in notImplemented() } // TODO: Implement } - private static func getSwarm(for hexEncodedPublicKey: String) -> Promise> { + private static func getSwarm(for hexEncodedPublicKey: String) -> Promise<[Target]> { if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount { - return Promise> { $0.fulfill(cachedSwarm) } + return Promise<[Target]> { $0.fulfill(cachedSwarm) } } else { - return getRandomSnode().then { invoke(.getSwarm, on: $0, with: [ "pubKey" : hexEncodedPublicKey ]) } - .map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } } } - private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise> { - return getSwarm(for: hexEncodedPublicKey).map { Set(Array($0).shuffled().prefix(targetSnodeCount)) } + private static func getTargetSnodes(for hexEncodedPublicKey: String) -> Promise<[Target]> { + // shuffled() uses the system's default random generator, which is cryptographically secure + return getSwarm(for: hexEncodedPublicKey).map { Array($0.shuffled().prefix(targetSnodeCount)) } } // MARK: Public API - public static func getMessages() -> Promise>>> { + public static func getMessages() -> Promise>> { let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in - let lastHash = getLastHash(for: targetSnode) ?? "" + let lastHash = getLastMessageHashValue(for: targetSnode) ?? "" let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ] return invoke(.getMessages, on: targetSnode, with: parameters).map { rawResponse in - if let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON], let lastMessage = messages.last, - let hash = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int { - setLastHash(for: targetSnode, hash: hash, expiresAt: UInt64(expiresAt)) - } - return parseProtoEnvelopes(from: rawResponse) + guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] } + updateLastMessageHashValueIfPossible(for: targetSnode, from: rawMessages) + let newRawMessages = removeDuplicates(from: rawMessages) + return parseProtoEnvelopes(from: newRawMessages) } }.map { Set($0) } } - public static func sendMessage(_ lokiMessage: Message) -> Promise<[Promise]> { + public static func sendMessage(_ lokiMessage: Message) -> Promise>> { let parameters = lokiMessage.toJSON() - return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters) - .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) } + return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) } } - public static func ping(_ hexEncodedPublicKey: String) -> Promise<[Promise]> { + public static func ping(_ hexEncodedPublicKey: String) -> Promise>> { let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] // TODO: Figure out correct parameters - return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters) - .recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) } + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) } } // MARK: Public API (Obj-C) @@ -99,43 +97,86 @@ import PromiseKit return anyPromise } - // MARK: Last Hash - private static func setLastHash(for target: Target, hash: String, expiresAt: UInt64) { - storage.dbReadWriteConnection.readWrite { transaction in - storage.setLastMessageHash(forServiceNode: target.address, hash: hash, expiresAt: expiresAt, transaction: transaction) + // 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 parseTargets(from rawResponse: Any) -> [Target] { + guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else { + Logger.warn("[Loki] Failed to parse targets from: \(rawResponse).") + return [] } + return addresses.map { Target(address: $0, port: defaultSnodePort) } } - private static func getLastHash(for target: Target) -> String? { - var lastHash: String? - storage.dbReadWriteConnection.readWrite { transaction in - lastHash = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) + private static func updateLastMessageHashValueIfPossible(for target: Target, from rawMessages: [JSON]) { + guard let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expiresAt = lastMessage["expiration"] as? Int else { + Logger.warn("[Loki] Failed to update last message hash value from: \(rawMessages).") + return } - return lastHash + setLastMessageHashValue(for: target, hashValue: hashValue, expiresAt: UInt64(expiresAt)) } - // MARK: Parsing - private static func parseTargets(from rawResponse: Any) -> Set { - guard let json = rawResponse as? JSON, let addresses = json["snodes"] as? [String] else { return [] } - return Set(addresses.map { Target(address: $0, port: defaultSnodePort) }) + private static func removeDuplicates(from rawMessages: [JSON]) -> [JSON] { + var receivedMessageHashValues = getReceivedMessageHashValues() + return rawMessages.filter { rawMessage in + guard let hashValue = rawMessage["hash"] as? String else { + Logger.warn("[Loki] Missing hash value for message: \(rawMessage).") + return false + } + let isDuplicate = receivedMessageHashValues.contains(hashValue) + receivedMessageHashValues.insert(hashValue) + setReceivedMessageHashValues(to: receivedMessageHashValues) + return !isDuplicate + } } - private static func parseProtoEnvelopes(from rawResponse: Any) -> Set { - guard let json = rawResponse as? JSON, let messages = json["messages"] as? [JSON] else { return [] } - return Set(messages.compactMap { message in - guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { - Logger.warn("[Loki] Failed to decode data for message: \(message).") + private static func parseProtoEnvelopes(from rawMessages: [JSON]) -> [SSKProtoEnvelope] { + return rawMessages.compactMap { rawMessage in + guard let base64EncodedData = rawMessage["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { + Logger.warn("[Loki] Failed to decode data for message: \(rawMessage).") return nil } guard let envelope = try? unwrap(data: data) else { - Logger.warn("[Loki] Failed to unwrap data for message: \(message).") + Logger.warn("[Loki] Failed to unwrap data for message: \(rawMessage).") return nil } return envelope - }) + } + } + + // MARK: Convenience + private static func getLastMessageHashValue(for target: Target) -> String? { + var result: String? = nil + // Uses a read/write connection because getting the last message hash value also removes expired messages as needed + storage.dbReadWriteConnection.readWrite { transaction in + result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) + } + return result + } + + private static func setLastMessageHashValue(for target: Target, hashValue: String, expiresAt: UInt64) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expiresAt, transaction: transaction) + } + } + + private static func getReceivedMessageHashValues() -> Set { + var result: Set = [] + storage.dbReadConnection.read { transaction in + result = storage.getReceivedMessageHashes(with: transaction) + } + return result + } + + private static func setReceivedMessageHashValues(to receivedMessageHashValues: Set) { + storage.dbReadWriteConnection.readWrite { transaction in + storage.setReceivedMessageHashes(receivedMessageHashValues, with: transaction) + } } } +// MARK: Error Handling private extension Promise { func recoverNetworkErrorIfNeeded(on queue: DispatchQueue) -> Promise { diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 59a552744..b99592924 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -72,7 +72,7 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction; -# pragma mark - Last Hash +# pragma mark - Last Hash Handling /** Get the last message hash for the given service node. @@ -95,6 +95,9 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(setLastMessageHash(forServiceNode:hash:expiresAt:transaction:)); +- (NSSet *)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction; +- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m index ff57f5b35..7ccefb5ab 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -10,11 +10,14 @@ #import "YapDatabaseConnection+OWS.h" #import "YapDatabaseTransaction+OWS.h" #import +#import "NSObject+Casting.h" #define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" -#define LokiPreKeyContactCollection @"LokiPreKeyContactCollection" -#define LokiPreKeyBundleCollection @"LokiPreKeyBundleCollection" -#define LokiLastHashCollection @"LokiLastHashCollection" +#define LKPreKeyContactCollection @"LKPreKeyContactCollection" +#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection" +#define LKLastMessageHashCollection @"LKLastMessageHashCollection" +#define LKReceivedMessageHashesKey @"LKReceivedMessageHashesKey" +#define LKReceivedMessageHashesCollection @"LKReceivedMessageHashesCollection" @implementation OWSPrimaryStorage (Loki) @@ -31,13 +34,13 @@ # pragma mark - Prekey for Contact - (BOOL)hasPreKeyForContact:(NSString *)pubKey { - int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection]; return preKeyId > 0; } - (PreKeyRecord *_Nullable)getPreKeyForContact:(NSString *)pubKey transaction:(YapDatabaseReadTransaction *)transaction { OWSAssertDebug(pubKey.length > 0); - int preKeyId = [transaction intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [transaction intForKey:pubKey inCollection:LKPreKeyContactCollection]; // If we don't have an id then return nil if (preKeyId <= 0) { return nil; } @@ -48,7 +51,7 @@ - (PreKeyRecord *)getOrCreatePreKeyForContact:(NSString *)pubKey { OWSAssertDebug(pubKey.length > 0); - int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LokiPreKeyContactCollection]; + int preKeyId = [self.dbReadWriteConnection intForKey:pubKey inCollection:LKPreKeyContactCollection]; // If we don't have an id then generate and store a new one if (preKeyId <= 0) { @@ -73,7 +76,7 @@ OWSAssertDebug(records.count > 0); PreKeyRecord *record = records.firstObject; - [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LokiPreKeyContactCollection]; + [self.dbReadWriteConnection setInt:record.Id forKey:pubKey inCollection:LKPreKeyContactCollection]; return record; } @@ -107,23 +110,23 @@ } - (PreKeyBundle *_Nullable)getPreKeyBundleForContact:(NSString *)pubKey { - return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LokiPreKeyBundleCollection]; + return [self.dbReadConnection preKeyBundleForKey:pubKey inCollection:LKPreKeyBundleCollection]; } - (void)setPreKeyBundle:(PreKeyBundle *)bundle forContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction { [transaction setObject:bundle forKey:pubKey - inCollection:LokiPreKeyBundleCollection]; + inCollection:LKPreKeyBundleCollection]; } - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction { - [transaction removeObjectForKey:pubKey inCollection:LokiPreKeyBundleCollection]; + [transaction removeObjectForKey:pubKey inCollection:LKPreKeyBundleCollection]; } # pragma mark - Last Hash - (NSString *_Nullable)getLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { - NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LokiLastHashCollection]; + NSDictionary *_Nullable dict = [transaction objectForKey:serviceNode inCollection:LKLastMessageHashCollection]; if (!dict) { return nil; } NSString *_Nullable hash = dict[@"hash"]; @@ -142,15 +145,20 @@ } - (void)setLastMessageHashForServiceNode:(NSString *)serviceNode hash:(NSString *)hash expiresAt:(u_int64_t)expiresAt transaction:(YapDatabaseReadWriteTransaction *)transaction { - NSDictionary *dict = @{ - @"hash": hash, - @"expiresAt": @(expiresAt) - }; - [transaction setObject:dict forKey:serviceNode inCollection:LokiLastHashCollection]; + NSDictionary *dict = @{ @"hash" : hash, @"expiresAt": @(expiresAt) }; + [transaction setObject:dict forKey:serviceNode inCollection:LKLastMessageHashCollection]; } - (void)removeLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { - [transaction removeObjectForKey:serviceNode inCollection:LokiLastHashCollection]; + [transaction removeObjectForKey:serviceNode inCollection:LKLastMessageHashCollection]; +} + +- (NSSet *)getReceivedMessageHashesWithTransaction:(YapDatabaseReadTransaction *)transaction { + return (NSSet *)[[transaction objectForKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection] as:NSSet.class]; +} + +- (void)setReceivedMessageHashes:(NSSet *)receivedMessageHashes withTransaction:(YapDatabaseReadWriteTransaction *)transaction { + [transaction setObject:receivedMessageHashes forKey:LKReceivedMessageHashesKey inCollection:LKReceivedMessageHashesCollection]; } @end