diff --git a/Pods b/Pods index 32ca94b0b..62d4d97c7 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 32ca94b0b45a1f55059ee500c5e217a0f55a0313 +Subproject commit 62d4d97c76c956b32957690d324145c18adf3279 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 3a7a05cd9..c2bf82e68 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1357,7 +1357,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; }; @@ -2628,7 +2628,7 @@ D221A08A169C9E5E00537ABF /* Products */ = { isa = PBXGroup; children = ( - D221A089169C9E5E00537ABF /* Session.app */, + D221A089169C9E5E00537ABF /* Signal.app */, D221A0AA169C9E5F00537ABF /* SignalTests.xctest */, 453518681FC635DD00210559 /* SignalShareExtension.appex */, 453518921FC63DBF00210559 /* SignalMessaging.framework */, @@ -2864,7 +2864,7 @@ ); name = Signal; productName = RedPhone; - productReference = D221A089169C9E5E00537ABF /* Session.app */; + productReference = D221A089169C9E5E00537ABF /* Signal.app */; productType = "com.apple.product-type.application"; }; D221A0A9169C9E5F00537ABF /* SignalTests */ = { @@ -4359,12 +4359,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; @@ -4424,11 +4425,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.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/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 2fc3cecb3..94ee2129e 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -1,20 +1,27 @@ import PromiseKit @objc public final class LokiAPI : NSObject { + private static let storage = OWSPrimaryStorage.shared() + // 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 - private enum Method : String { - case getMessages = "retrieve" - case sendMessage = "store" - case getSwarm = "get_snodes_for_pubkey" - } + // MARK: Caching + private static var swarmCache: [String:[Target]] = [:] - public struct Target : Hashable { + // MARK: Types + 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,69 +39,148 @@ 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 { - return Promise { seal in - seal.fulfill(Target(address: "http://13.238.53.205", port: 8080)) // TODO: Temporary - } + private static func getRandomSnode() -> Promise { + return Promise { _ in notImplemented() } // TODO: Implement } - 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? LokiMessageWrapper.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<[Target]> { + if let cachedSwarm = swarmCache[hexEncodedPublicKey], cachedSwarm.count >= targetSnodeCount { + return Promise<[Target]> { $0.fulfill(cachedSwarm) } + } else { + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey ] + return getRandomSnode().then { invoke(.getSwarm, on: $0, with: parameters) }.map { parseTargets(from: $0) }.get { swarmCache[hexEncodedPublicKey] = $0 } } } - public static func sendMessage(_ lokiMessage: Message) -> Promise { - return getRandomSnode().then { invoke(.sendMessage, on: $0, with: lokiMessage.toJSON()) } // TODO: Use getSwarm() + 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>> { + let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in + let lastHash = getLastMessageHashValue(for: targetSnode) ?? "" + let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ] + return invoke(.getMessages, on: targetSnode, with: parameters).map { rawResponse in + 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 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 sendMessage(_ lokiMessage: Message) -> Promise>> { + let parameters = lokiMessage.toJSON() + return getTargetSnodes(for: lokiMessage.destination).mapValues { invoke(.sendMessage, on: $0, with: parameters).recoverNetworkErrorIfNeeded(on: DispatchQueue.global()) }.map { Set($0) } } - 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 + 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()) }.map { Set($0) } } - // 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) let anyPromise = AnyPromise(promise) anyPromise.retainUntilComplete() return anyPromise } + + // 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 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 + } + setLastMessageHashValue(for: target, hashValue: hashValue, expiresAt: UInt64(expiresAt)) + } + + 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 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? LokiMessageWrapper.unwrap(data: data) else { + 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: - Convenience - +// MARK: Error Handling 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 diff --git a/SignalServiceKit/src/Loki/API/LokiMessageWrapper.swift b/SignalServiceKit/src/Loki/API/LokiMessageWrapper.swift index c8fa1e8a8..1b9ccaefb 100644 --- a/SignalServiceKit/src/Loki/API/LokiMessageWrapper.swift +++ b/SignalServiceKit/src/Loki/API/LokiMessageWrapper.swift @@ -36,22 +36,6 @@ public class LokiMessageWrapper { } } - /// 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. - public static func unwrap(data: Data) throws -> SSKProtoEnvelope { - do { - let webSocketMessage = try WebSocketProtoWebSocketMessage.parseData(data) - let envelope = webSocketMessage.request!.body! - return try SSKProtoEnvelope.parseData(envelope) - } catch let error { - 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 { @@ -92,4 +76,20 @@ public class LokiMessageWrapper { 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/Crypto/OWSPrimaryStorage+Loki.h b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h index 647fafc88..b99592924 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.h @@ -72,6 +72,32 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)removePreKeyBundleForContact:(NSString *)pubKey transaction:(YapDatabaseReadWriteTransaction *)transaction; +# pragma mark - Last Hash Handling + +/** + Get the last message hash for the given service node. + 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. + */ +- (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 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)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 125679a53..7ccefb5ab 100644 --- a/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m +++ b/SignalServiceKit/src/Loki/Crypto/OWSPrimaryStorage+Loki.m @@ -4,15 +4,20 @@ #import "OWSPrimaryStorage+keyFromIntLong.h" #import "OWSDevice.h" #import "OWSIdentityManager.h" +#import "NSDate+OWS.h" #import "TSAccountManager.h" #import "TSPreKeyManager.h" #import "YapDatabaseConnection+OWS.h" #import "YapDatabaseTransaction+OWS.h" #import +#import "NSObject+Casting.h" #define OWSPrimaryStoragePreKeyStoreCollection @"TSStorageManagerPreKeyStoreCollection" -#define LokiPreKeyContactCollection @"LokiPreKeyContactCollection" -#define LokiPreKeyBundleCollection @"LokiPreKeyBundleCollection" +#define LKPreKeyContactCollection @"LKPreKeyContactCollection" +#define LKPreKeyBundleCollection @"LKPreKeyBundleCollection" +#define LKLastMessageHashCollection @"LKLastMessageHashCollection" +#define LKReceivedMessageHashesKey @"LKReceivedMessageHashesKey" +#define LKReceivedMessageHashesCollection @"LKReceivedMessageHashesCollection" @implementation OWSPrimaryStorage (Loki) @@ -29,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; } @@ -46,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) { @@ -71,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; } @@ -105,17 +110,55 @@ } - (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:LKLastMessageHashCollection]; + 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)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:LKLastMessageHashCollection]; +} + +- (void)removeLastMessageHashForServiceNode:(NSString *)serviceNode transaction:(YapDatabaseReadWriteTransaction *)transaction { + [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 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) + } +}