From 77b67e73bbd328fb7bce15fea646dd1abfcde8fc Mon Sep 17 00:00:00 2001 From: Mikunj Date: Fri, 10 May 2019 12:41:49 +1000 Subject: [PATCH] Refactor LokiAPI --- Pods | 2 +- .../src/Loki/Api/LokiAPI+Message.swift | 91 ++++++++++++ .../src/Loki/Api/LokiAPI+Wrapping.swift | 116 +++++++++++++++ SignalServiceKit/src/Loki/Api/LokiAPI.swift | 27 ++-- .../src/Loki/Api/LokiMessage.swift | 134 ------------------ 5 files changed, 224 insertions(+), 146 deletions(-) create mode 100644 SignalServiceKit/src/Loki/Api/LokiAPI+Message.swift create mode 100644 SignalServiceKit/src/Loki/Api/LokiAPI+Wrapping.swift delete mode 100644 SignalServiceKit/src/Loki/Api/LokiMessage.swift diff --git a/Pods b/Pods index 51a2b3e86..d80917fe8 160000 --- a/Pods +++ b/Pods @@ -1 +1 @@ -Subproject commit 51a2b3e8610c1083db88502aaa76da1f352757da +Subproject commit d80917fe856943d30fe1c7a956603368b77a00dc diff --git a/SignalServiceKit/src/Loki/Api/LokiAPI+Message.swift b/SignalServiceKit/src/Loki/Api/LokiAPI+Message.swift new file mode 100644 index 000000000..555dabfb1 --- /dev/null +++ b/SignalServiceKit/src/Loki/Api/LokiAPI+Message.swift @@ -0,0 +1,91 @@ +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. + let ttl: UInt64 + /// When the proof of work was calculated, if applicable. + /// + /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. + let timestamp: UInt64? + /// The base 64 encoded proof of work, if applicable. + let nonce: String? + + /// Create a Message + /// + /// - Parameters: + /// - destination: A hex encoded public key + /// - data: The content of the message + /// - ttl: Time to live in seconds + /// - timestamp: Timestamp used when calculating proof of work. Expressed in milliseconds. + /// - nonce: The base 64 encoded proof of work + public init(destination: String, data: LosslessStringConvertible, ttl: UInt64, timestamp: UInt64?, nonce: String?) { + self.destination = destination + self.data = data + self.ttl = ttl + self.timestamp = timestamp + self.nonce = nonce + } + + /// Build a LokiMessage from a SignalMessage + /// + /// - Parameters: + /// - signalMessage: the signal message + /// - timestamp: the original message timestamp (TSOutgoingMessage.timestamp) + /// - isPoWRequired: Should we calculate proof of work + /// - Returns: The loki message + public static func from(signalMessage: SignalMessage, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> Promise { + // To match the desktop application we have to take the data + // wrap it in an envelope, then + // wrap it in a websocket + return Promise { seal in + DispatchQueue.global(qos: .default).async { + do { + let serialized = try wrap(message: signalMessage, timestamp: timestamp) + let data = serialized.base64EncodedString() + let destination = signalMessage["destination"] as! String + let ttl = LokiAPI.defaultMessageTTL + + if isPoWRequired { + // timeIntervalSince1970 returns timestamp in seconds but the storage server only accepts timestamp in milliseconds + let now = UInt64(Date().timeIntervalSince1970 * 1000) + if let nonce = ProofOfWork.calculate(data: data, pubKey: destination, timestamp: now, ttl: ttl) { + let result = Message(destination: destination, data: data, ttl: ttl, timestamp: now, nonce: nonce) + seal.fulfill(result) + } else { + seal.reject(Error.proofOfWorkCalculationFailed) + } + } else { + let result = Message(destination: destination, data: data, ttl: ttl, timestamp: nil, nonce: nil) + seal.fulfill(result) + } + } catch let error { + seal.reject(error) + } + } + } + } + + 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 + } + } + + +} + + +private extension LokiAPI.Message { + +} + diff --git a/SignalServiceKit/src/Loki/Api/LokiAPI+Wrapping.swift b/SignalServiceKit/src/Loki/Api/LokiAPI+Wrapping.swift new file mode 100644 index 000000000..24046fa6a --- /dev/null +++ b/SignalServiceKit/src/Loki/Api/LokiAPI+Wrapping.swift @@ -0,0 +1,116 @@ +/// A helper util class for the api +extension LokiAPI { + + // Custom erros for us + enum WrappingError : LocalizedError { + case failedToWrapData + case failedToWrapEnvelope + case failedToWrapWebSocket + case failedToUnwrapData + + public var errorDescription: String? { + switch self { + case .failedToWrapData: return "Failed to wrap data" + case .failedToWrapEnvelope: return NSLocalizedString("Failed to wrap data in an Envelope", comment: "") + case .failedToWrapWebSocket: return NSLocalizedString("Failed to wrap data in an WebSocket", comment: "") + case .failedToUnwrapData: return "Failed to unwrap data" + } + } + } + + /// Wrap a message for sending to the storage server. + /// This will wrap the message in an Envelope and then a WebSocket to match the desktop application. + /// + /// - Parameters: + /// - message: The signal message + /// - timestamp: The original message timestamp (TSOutgoingMessage.timestamp) + /// - Returns: The wrapped message data + /// - Throws: WrappingError if something went wrong + static func wrap(message: SignalMessage, timestamp: UInt64) throws -> Data { + do { + let envelope = try buildEnvelope(from: message, timestamp: timestamp) + let websocket = try buildWebSocket(from: envelope) + return try websocket.serializedData() + } catch let error { + if !(error is WrappingError) { + throw WrappingError.failedToWrapData + } else { + throw error + } + } + + } + + /// Unwrap data from the storage server + /// + /// - Parameter data: The data from the storage server (not base64 encoded) + /// - Returns: The envelope + /// - Throws: 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 API - failed unwrapping message: \(error)") + throw WrappingError.failedToUnwrapData + } + } + + /// Wrap EnvelopeProto in a WebSocketProto + /// This is needed because it is done automatically on the desktop + private static func buildWebSocket(from envelope: SSKProtoEnvelope) throws -> WebSocketProtoWebSocketMessage { + do { + // This request is just a copy of the one on desktop + let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1.. SSKProtoEnvelope { + guard let ourKeys = SSKEnvironment.shared.identityManager.identityKeyPair() else { + owsFailDebug("Loki API - error building envelope: identityManager.identityKeyPair() is invalid") + throw WrappingError.failedToWrapEnvelope + } + + do { + let ourPubKey = ourKeys.hexEncodedPublicKey + + let params = ParamParser(dictionary: signalMessage) + + let typeInt: Int32 = try params.required(key: "type") + guard let type: SSKProtoEnvelope.SSKProtoEnvelopeType = SSKProtoEnvelope.SSKProtoEnvelopeType(rawValue: typeInt) else { + Logger.error("`type` was invalid: \(typeInt)") + throw ParamParser.ParseError.invalidFormat("type") + } + + let builder = SSKProtoEnvelope.builder(type: type, timestamp: timestamp) + builder.setSource(ourPubKey) + builder.setSourceDevice(OWSDevicePrimaryDeviceId) + + if let content = try params.optionalBase64EncodedData(key: "content") { + builder.setContent(content) + } + + return try builder.build() + } catch let error { + owsFailDebug("Loki Message: error building envelope: \(error)") + throw WrappingError.failedToWrapEnvelope + } + } + + +} diff --git a/SignalServiceKit/src/Loki/Api/LokiAPI.swift b/SignalServiceKit/src/Loki/Api/LokiAPI.swift index 073fda616..8976a2fd8 100644 --- a/SignalServiceKit/src/Loki/Api/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/Api/LokiAPI.swift @@ -54,20 +54,25 @@ import PromiseKit "pubKey" : OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey, "lastHash" : "" // TODO: Implement ] - return getRandomSnode().then { invoke(.getMessages, on: $0, with: parameters) }.map { rawResponse in - guard let json = rawResponse as? [String:Any] else { fatalError() } // TODO: Use JSON type; handle error - guard let messages = json["messages"] as? [[String:Any]] else { fatalError() } // TODO: Use JSON type; handle error - return messages.map { message in - guard let base64EncodedData = message["data"] as? String else { fatalError() } // TODO: Handle error - let data = Data(base64Encoded: base64EncodedData)! // TODO: Handle error - let webSocketMessage = try! WebSocketProtoWebSocketMessage.parseData(data) - let envelope = webSocketMessage.request!.body! // TODO: Handle error - return try! SSKProtoEnvelope.parseData(envelope) // TODO: Handle error + return getRandomSnode().then { invoke(.getMessages, on: $0, with: parameters) }.compactMap { rawResponse in + guard let json = rawResponse as? [String:Any], let messages = json["messages"] as? [[String:Any]] else { return nil } + return messages.compactMap { message in + guard let base64EncodedData = message["data"] as? String, let data = Data(base64Encoded: base64EncodedData) else { + Logger.warn("LokiAPI - Failed to get data for message: \(message)") + return nil + } + + guard let envelope = try? unwrap(data: data) else { + Logger.warn("LokiAPI - Failed to unwrap data for message: \(message)") + return nil + } + + return envelope } } } - public static func sendMessage(_ lokiMessage: LokiMessage) -> Promise { + public static func sendMessage(_ lokiMessage: Message) -> Promise { return getRandomSnode().then { invoke(.sendMessage, on: $0, with: lokiMessage.toJSON()) } // TODO: Use getSwarm() } @@ -88,7 +93,7 @@ import PromiseKit } @objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> AnyPromise { - let promise = LokiMessage.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired) + let promise = Message.from(signalMessage: signalMessage, timestamp: timestamp, requiringPoW: isPoWRequired) .then(sendMessage) .recoverNetworkError(on: DispatchQueue.global()) let anyPromise = AnyPromise(promise) diff --git a/SignalServiceKit/src/Loki/Api/LokiMessage.swift b/SignalServiceKit/src/Loki/Api/LokiMessage.swift deleted file mode 100644 index 53730a66e..000000000 --- a/SignalServiceKit/src/Loki/Api/LokiMessage.swift +++ /dev/null @@ -1,134 +0,0 @@ -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. - let ttl: UInt64 - /// When the proof of work was calculated, if applicable. - /// - /// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970. - let timestamp: UInt64? - /// The base 64 encoded proof of work, if applicable. - let nonce: String? - - public init(destination: String, data: LosslessStringConvertible, ttl: UInt64, timestamp: UInt64?, nonce: String?) { - self.destination = destination - self.data = data - self.ttl = ttl - self.timestamp = timestamp - self.nonce = nonce - } - - /// Build a LokiMessage from a SignalMessage - /// - /// - Parameters: - /// - signalMessage: the signal message - /// - timestamp: the original message timestamp (TSOutgoingMessage.timestamp) - /// - isPoWRequired: Should we calculate proof of work - /// - Returns: The loki message - public static func from(signalMessage: SignalMessage, timestamp: UInt64, requiringPoW isPoWRequired: Bool) -> Promise { - // To match the desktop application we have to take the data - // wrap it in an envelope, then - // wrap it in a websocket - return Promise { seal in - DispatchQueue.global(qos: .default).async { - guard let envelope = buildEnvelope(fromSignalMessage: signalMessage, timestamp: timestamp) else { - seal.reject(LokiAPI.Error.failedToWrapInEnvelope) - return - } - - // Make the data - guard let websocket = wrapInWebsocket(envelope: envelope), - let serialized = try? websocket.serializedData() else { - seal.reject(LokiAPI.Error.failedToWrapInWebSocket) - return; - } - - let data = serialized.base64EncodedString() - let destination = signalMessage["destination"] as! String - let ttl = LokiAPI.defaultMessageTTL - - if isPoWRequired { - // timeIntervalSince1970 returns timestamp in seconds but the storage server only accepts timestamp in milliseconds - let now = UInt64(Date().timeIntervalSince1970 * 1000) - if let nonce = ProofOfWork.calculate(data: data, pubKey: destination, timestamp: now, ttl: ttl) { - let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: now, nonce: nonce) - seal.fulfill(result) - } else { - seal.reject(LokiAPI.Error.proofOfWorkCalculationFailed) - } - } else { - let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: nil, nonce: nil) - seal.fulfill(result) - } - } - } - } - - /// Wrap EnvelopeProto in a WebSocketProto - /// This is needed because it is done automatically on the desktop - private static func wrapInWebsocket(envelope: SSKProtoEnvelope) -> WebSocketProtoWebSocketMessage? { - do { - // This request is just a copy of the one on desktop - let requestBuilder = WebSocketProtoWebSocketRequestMessage.builder(verb: "PUT", path: "/api/v1/message", requestID: UInt64.random(in: 1.. SSKProtoEnvelope? { - guard let ourKeys = SSKEnvironment.shared.identityManager.identityKeyPair() else { - owsFailDebug("error building envelope: identityManager.identityKeyPair() is invalid") - return nil; - } - - do { - let ourPubKey = ourKeys.hexEncodedPublicKey - - let params = ParamParser(dictionary: signalMessage) - - let typeInt: Int32 = try params.required(key: "type") - guard let type: SSKProtoEnvelope.SSKProtoEnvelopeType = SSKProtoEnvelope.SSKProtoEnvelopeType(rawValue: typeInt) else { - Logger.error("`type` was invalid: \(typeInt)") - throw ParamParser.ParseError.invalidFormat("type") - } - - let builder = SSKProtoEnvelope.builder(type: type, timestamp: timestamp) - builder.setSource(ourPubKey) - builder.setSourceDevice(OWSDevicePrimaryDeviceId) - - if let content = try params.optionalBase64EncodedData(key: "content") { - builder.setContent(content) - } - - return try builder.build() - } catch { - owsFailDebug("Loki Message: error building envelope: \(error)") - return nil - } - } - - 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 - } -}