diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift index c4ec84e8e..2810e4777 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+Convenience.swift @@ -8,15 +8,16 @@ internal extension LokiAPI { internal static func getLastMessageHashValue(for target: LokiAPITarget) -> String? { var result: String? = nil // Uses a read/write connection because getting the last message hash value also removes expired messages as needed + // TODO: This shouldn't be the case; a getter shouldn't have an unexpected side effect storage.dbReadWriteConnection.readWrite { transaction in result = storage.getLastMessageHash(forServiceNode: target.address, transaction: transaction) } return result } - internal static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expiresAt: UInt64) { + internal static func setLastMessageHashValue(for target: LokiAPITarget, hashValue: String, expirationDate: UInt64) { storage.dbReadWriteConnection.readWrite { transaction in - storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expiresAt: expiresAt, transaction: transaction) + storage.setLastMessageHash(forServiceNode: target.address, hash: hashValue, expirationDate: expirationDate, transaction: transaction) } } diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift b/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift index 515554c9c..ca4f263ab 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI+LongPolling.swift @@ -84,7 +84,7 @@ public extension LokiAPI { func getMessagesInfinitely(from target: LokiAPITarget) -> Promise { // The only way to exit the infinite loop is to throw an error 3 times or cancel - return getRawMessages(from: target, useLongPolling: true).then { rawResponse -> Promise in + return getRawMessages(from: target, usingLongPolling: true).then { rawResponse -> Promise in // Check if we need to abort guard !isCancelled else { throw PMKError.cancelled } diff --git a/SignalServiceKit/src/Loki/API/LokiAPI+Message.swift b/SignalServiceKit/src/Loki/API/LokiAPI+Message.swift deleted file mode 100644 index 8868a8403..000000000 --- a/SignalServiceKit/src/Loki/API/LokiAPI+Message.swift +++ /dev/null @@ -1,78 +0,0 @@ -import PromiseKit - -public extension LokiAPI { - - public struct Message { - /// The hex encoded public key of the receiver. - let destination: String - /// The content of the message. - let data: LosslessStringConvertible - /// The time to live for the message in milliseconds. - let ttl: UInt64 - /// Whether this message is a ping. - /// - /// - Note: The concept of pinging only applies to P2P messaging. - let isPing: Bool - /// When the proof of work was calculated, if applicable (P2P messages don't require proof of work). - /// - /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. - private(set) var timestamp: UInt64? = nil - /// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work). - private(set) var nonce: String? = nil - - private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) { - self.destination = destination - self.data = data - self.ttl = ttl - self.isPing = isPing - } - - /// Construct a `LokiMessage` from a `SignalMessage`. - /// - /// - Note: `timestamp` is the original message timestamp (i.e. `TSOutgoingMessage.timestamp`). - public static func from(signalMessage: SignalMessage) -> Message? { - // To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object - do { - let wrappedMessage = try LokiMessageWrapper.wrap(message: signalMessage) - let data = wrappedMessage.base64EncodedString() - let destination = signalMessage.recipientID - var ttl = LokiAPI.defaultMessageTTL - if let messageTTL = signalMessage.ttl, messageTTL > 0 { ttl = UInt64(messageTTL) } - let isPing = signalMessage.isPing - return Message(destination: destination, data: data, ttl: ttl, isPing: isPing) - } catch let error { - Logger.debug("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).") - return nil - } - } - - /// Calculate the proof of work for this message. - /// - /// - Returns: The promise of a new message with its `timestamp` and `nonce` set. - public func calculatePoW() -> Promise { - return Promise { seal in - DispatchQueue.global(qos: .default).async { - let now = NSDate.ows_millisecondTimeStamp() - let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented - if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) { - var result = self - result.timestamp = now - result.nonce = nonce - seal.fulfill(result) - } else { - seal.reject(Error.proofOfWorkCalculationFailed) - } - } - } - } - - public func toJSON() -> JSON { - var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ] - if let timestamp = timestamp, let nonce = nonce { - result["timestamp"] = String(timestamp) - result["nonce"] = nonce - } - return result - } - } -} diff --git a/SignalServiceKit/src/Loki/API/LokiAPI.swift b/SignalServiceKit/src/Loki/API/LokiAPI.swift index a3ba6e67b..c75c8e62d 100644 --- a/SignalServiceKit/src/Loki/API/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/API/LokiAPI.swift @@ -3,6 +3,8 @@ import PromiseKit @objc public final class LokiAPI : NSObject { internal static let storage = OWSPrimaryStorage.shared() + private static var userPublicKey: String { return OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey } + // MARK: Settings private static let version = "v1" private static let maxRetryCount: UInt = 3 @@ -42,27 +44,25 @@ import PromiseKit .handlingSwarmSpecificErrorsIfNeeded(for: target, associatedWith: hexEncodedPublicKey).recoveringNetworkErrorsIfNeeded() } - internal static func getRawMessages(from target: LokiAPITarget, useLongPolling: Bool) -> RawResponsePromise { - let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey + internal static func getRawMessages(from target: LokiAPITarget, usingLongPolling useLongPolling: Bool) -> RawResponsePromise { let lastHashValue = getLastMessageHashValue(for: target) ?? "" - let parameters = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHashValue ] + let parameters = [ "pubKey" : userPublicKey, "lastHash" : lastHashValue ] let headers: [String:String]? = useLongPolling ? [ "X-Loki-Long-Poll" : "true" ] : nil let timeout: TimeInterval? = useLongPolling ? longPollingTimeout : nil - return invoke(.getMessages, on: target, associatedWith: hexEncodedPublicKey, parameters: parameters, headers: headers, timeout: timeout) + return invoke(.getMessages, on: target, associatedWith: userPublicKey, parameters: parameters, headers: headers, timeout: timeout) } // MARK: Public API public static func getMessages() -> Promise> { - let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey - return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in - return getRawMessages(from: targetSnode, useLongPolling: false).map { parseRawMessagesResponse($0, from: targetSnode) } + return getTargetSnodes(for: userPublicKey).mapValues { targetSnode in + return getRawMessages(from: targetSnode, usingLongPolling: false).map { parseRawMessagesResponse($0, from: targetSnode) } }.map { Set($0) }.retryingIfNeeded(maxRetryCount: maxRetryCount) } public static func sendSignalMessage(_ signalMessage: SignalMessage, onP2PSuccess: @escaping () -> Void) -> Promise> { - guard let lokiMessage = Message.from(signalMessage: signalMessage) else { return Promise(error: Error.messageConversionFailed) } + guard let lokiMessage = LokiMessage.from(signalMessage: signalMessage) else { return Promise(error: Error.messageConversionFailed) } let destination = lokiMessage.destination - func sendLokiMessage(_ lokiMessage: Message, to target: LokiAPITarget) -> RawResponsePromise { + func sendLokiMessage(_ lokiMessage: LokiMessage, to target: LokiAPITarget) -> RawResponsePromise { let parameters = lokiMessage.toJSON() return invoke(.sendMessage, on: target, associatedWith: destination, parameters: parameters) } @@ -116,7 +116,7 @@ import PromiseKit private static func updateLastMessageHashValueIfPossible(for target: LokiAPITarget, from rawMessages: [JSON]) { if let lastMessage = rawMessages.last, let hashValue = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? Int { - setLastMessageHashValue(for: target, hashValue: hashValue, expiresAt: UInt64(expirationDate)) + setLastMessageHashValue(for: target, hashValue: hashValue, expirationDate: UInt64(expirationDate)) } else if (!rawMessages.isEmpty) { Logger.warn("[Loki] Failed to update last message hash value from: \(rawMessages).") } diff --git a/SignalServiceKit/src/Loki/API/LokiMessage.swift b/SignalServiceKit/src/Loki/API/LokiMessage.swift new file mode 100644 index 000000000..23f15f30c --- /dev/null +++ b/SignalServiceKit/src/Loki/API/LokiMessage.swift @@ -0,0 +1,75 @@ +import PromiseKit + +public struct LokiMessage { + /// The hex encoded public key of the receiver. + let destination: String + /// The content of the message. + let data: LosslessStringConvertible + /// The time to live for the message in milliseconds. + let ttl: UInt64 + /// Whether this message is a ping. + /// + /// - Note: The concept of pinging only applies to P2P messaging. + let isPing: Bool + /// When the proof of work was calculated, if applicable (P2P messages don't require proof of work). + /// + /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. + private(set) var timestamp: UInt64? = nil + /// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work). + private(set) var nonce: String? = nil + + private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool) { + self.destination = destination + self.data = data + self.ttl = ttl + self.isPing = isPing + } + + /// Construct a `LokiMessage` from a `SignalMessage`. + /// + /// - Note: `timestamp` is the original message timestamp (i.e. `TSOutgoingMessage.timestamp`). + public static func from(signalMessage: SignalMessage) -> LokiMessage? { + // To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object + do { + let wrappedMessage = try LokiMessageWrapper.wrap(message: signalMessage) + let data = wrappedMessage.base64EncodedString() + let destination = signalMessage.recipientID + var ttl = LokiAPI.defaultMessageTTL + if let messageTTL = signalMessage.ttl, messageTTL > 0 { ttl = UInt64(messageTTL) } + let isPing = signalMessage.isPing + return LokiMessage(destination: destination, data: data, ttl: ttl, isPing: isPing) + } catch let error { + Logger.debug("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).") + return nil + } + } + + /// Calculate the proof of work for this message. + /// + /// - Returns: The promise of a new message with its `timestamp` and `nonce` set. + public func calculatePoW() -> Promise { + return Promise { seal in + DispatchQueue.global(qos: .default).async { + let now = NSDate.ows_millisecondTimeStamp() + let dataAsString = self.data as! String // Safe because of how from(signalMessage:with:) is implemented + if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) { + var result = self + result.timestamp = now + result.nonce = nonce + seal.fulfill(result) + } else { + seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed) + } + } + } + } + + public func toJSON() -> JSON { + var result = [ "pubKey" : destination, "data" : data.description, "ttl" : String(ttl) ] + if let timestamp = timestamp, let nonce = nonce { + result["timestamp"] = String(timestamp) + result["nonce"] = nonce + } + return result + } +} diff --git a/SignalServiceKit/src/Loki/API/JSON.swift b/SignalServiceKit/src/Loki/Utilities/JSON.swift similarity index 100% rename from SignalServiceKit/src/Loki/API/JSON.swift rename to SignalServiceKit/src/Loki/Utilities/JSON.swift