diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index c1c50bf36..84fb46c33 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -2570,3 +2570,5 @@ "No search results" = "No search results"; "Calculating proof of work" = "Calculating proof of work"; "Failed to calculate proof of work." = "Failed to calculate proof of work."; +"Failed to wrap data in an Envelope" = "Failed to wrap data in an Envelope."; +"Failed to wrap data in an WebSocket" = "Failed to wrap data in an WebSocket."; diff --git a/SignalServiceKit/src/Loki/Api/LokiAPI.swift b/SignalServiceKit/src/Loki/Api/LokiAPI.swift index 08df47ded..491644e2c 100644 --- a/SignalServiceKit/src/Loki/Api/LokiAPI.swift +++ b/SignalServiceKit/src/Loki/Api/LokiAPI.swift @@ -21,10 +21,14 @@ import PromiseKit public enum Error : LocalizedError { case proofOfWorkCalculationFailed + case failedToWrapInEnvelope + case failedToWrapInWebSocket public var errorDescription: String? { switch self { case .proofOfWorkCalculationFailed: return NSLocalizedString("Failed to calculate proof of work.", comment: "") + case .failedToWrapInEnvelope: return NSLocalizedString("Failed to wrap data in an Envelope", comment: "") + case .failedToWrapInWebSocket: return NSLocalizedString("Failed to wrap data in an WebSocket", comment: "") } } } @@ -73,8 +77,8 @@ import PromiseKit return anyPromise } - @objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, requiringPoW isPoWRequired: Bool) -> AnyPromise { - let promise = LokiMessage.fromSignalMessage(signalMessage, requiringPoW: isPoWRequired) + @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) .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 index 889960a49..53730a66e 100644 --- a/SignalServiceKit/src/Loki/Api/LokiMessage.swift +++ b/SignalServiceKit/src/Loki/Api/LokiMessage.swift @@ -22,17 +22,40 @@ public struct LokiMessage { self.nonce = nonce } - public static func fromSignalMessage(_ signalMessage: SignalMessage, requiringPoW isPoWRequired: Bool) -> Promise { + /// 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 data = signalMessage["content"] as! String let ttl = LokiAPI.defaultMessageTTL + if isPoWRequired { // timeIntervalSince1970 returns timestamp in seconds but the storage server only accepts timestamp in milliseconds - let timestamp = UInt64(Date().timeIntervalSince1970 * 1000) - if let nonce = ProofOfWork.calculate(data: data, pubKey: destination, timestamp: timestamp, ttl: ttl) { - let result = LokiMessage(destination: destination, data: data, ttl: ttl, timestamp: timestamp, nonce: nonce) + 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) @@ -45,6 +68,61 @@ public struct LokiMessage { } } + /// 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 { diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index 9b0b7052f..21ac2ea47 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -1112,7 +1112,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // Convert the message to a Loki message and send it using the Loki messaging API NSDictionary *signalMessage = deviceMessages.firstObject; BOOL isPoWRequired = YES; // TODO: Base on message type - [[LokiAPI objc_sendSignalMessage:signalMessage to:recipient.recipientId requiringPoW:isPoWRequired] + [[LokiAPI objc_sendSignalMessage:signalMessage to:recipient.recipientId timestamp:message.timestamp requiringPoW:isPoWRequired] .thenOn(OWSDispatch.sendingQueue, ^(id result) { [self messageSendDidSucceed:messageSend deviceMessages:deviceMessages