From 428daac5b3f1337799650964510e85d734f4b51c Mon Sep 17 00:00:00 2001 From: Ryan ZHAO <> Date: Tue, 21 Jul 2020 14:51:15 +1000 Subject: [PATCH] use onion routing for file server requests --- .../src/Loki/API/FileServerAPI.swift | 106 +++++++++++------- .../OnionRequestAPI+Encryption.swift | 21 ++-- .../API/Onion Requests/OnionRequestAPI.swift | 47 +++++++- 3 files changed, 125 insertions(+), 49 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index 1472c916a..a0772c3bc 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -44,52 +44,69 @@ public final class FileServerAPI : DotNetAPI { public static func getDeviceLinks(associatedWith hexEncodedPublicKeys: Set<String>) -> Promise<Set<DeviceLink>> { let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]" print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).") + + func handleRawResponseForDeviceLinks(rawResponse: JSON, data: [JSON]) -> Set<DeviceLink> { + return Set(data.flatMap { data -> [DeviceLink] in + guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] } + guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }), + let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON], + let hexEncodedPublicKey = data["username"] as? String else { + print("[Loki] Couldn't parse device links from: \(rawResponse).") + return [] + } + return rawDeviceLinks.compactMap { rawDeviceLink in + guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String, + let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else { + print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).") + return nil + } + let masterSignature: Data? + if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String { + masterSignature = Data(base64Encoded: base64EncodedMasterSignature) + } else { + masterSignature = nil + } + let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature) + let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature) + let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature) + let deviceLink = DeviceLink(between: master, and: slave) + if let masterSignature = masterSignature { + guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid master signature.") + return nil + } + } + guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { + print("[Loki] Received a device link with an invalid slave signature.") + return nil + } + return deviceLink + } + }) + } + return getAuthToken(for: server).then2 { token -> Promise<Set<DeviceLink>> in let queryParameters = "ids=\(hexEncodedPublicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1" let url = URL(string: "\(server)/users?\(queryParameters)")! let request = TSRequest(url: url) + if (useOnionRequests) { + return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { rawResponse -> Set<DeviceLink> in + guard let data = rawResponse["data"] as? [JSON] else { + print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).") + throw DotNetAPIError.parsingFailed + } + return handleRawResponseForDeviceLinks(rawResponse: rawResponse, data: data) + }.map2 { deviceLinks in + storage.setDeviceLinks(deviceLinks) + return deviceLinks + } + } return LokiFileServerProxy(for: server).perform(request, withCompletionQueue: DispatchQueue.global(qos: .default)).map2 { rawResponse -> Set<DeviceLink> in guard let json = rawResponse as? JSON, let data = json["data"] as? [JSON] else { print("[Loki] Couldn't parse device links for users: \(hexEncodedPublicKeys) from: \(rawResponse).") throw DotNetAPIError.parsingFailed } - return Set(data.flatMap { data -> [DeviceLink] in - guard let annotations = data["annotations"] as? [JSON], !annotations.isEmpty else { return [] } - guard let annotation = annotations.first(where: { $0["type"] as? String == deviceLinkType }), - let value = annotation["value"] as? JSON, let rawDeviceLinks = value["authorisations"] as? [JSON], - let hexEncodedPublicKey = data["username"] as? String else { - print("[Loki] Couldn't parse device links from: \(rawResponse).") - return [] - } - return rawDeviceLinks.compactMap { rawDeviceLink in - guard let masterHexEncodedPublicKey = rawDeviceLink["primaryDevicePubKey"] as? String, let slaveHexEncodedPublicKey = rawDeviceLink["secondaryDevicePubKey"] as? String, - let base64EncodedSlaveSignature = rawDeviceLink["requestSignature"] as? String else { - print("[Loki] Couldn't parse device link for user: \(hexEncodedPublicKey) from: \(rawResponse).") - return nil - } - let masterSignature: Data? - if let base64EncodedMasterSignature = rawDeviceLink["grantSignature"] as? String { - masterSignature = Data(base64Encoded: base64EncodedMasterSignature) - } else { - masterSignature = nil - } - let slaveSignature = Data(base64Encoded: base64EncodedSlaveSignature) - let master = DeviceLink.Device(hexEncodedPublicKey: masterHexEncodedPublicKey, signature: masterSignature) - let slave = DeviceLink.Device(hexEncodedPublicKey: slaveHexEncodedPublicKey, signature: slaveSignature) - let deviceLink = DeviceLink(between: master, and: slave) - if let masterSignature = masterSignature { - guard DeviceLinkingUtilities.hasValidMasterSignature(deviceLink) else { - print("[Loki] Received a device link with an invalid master signature.") - return nil - } - } - guard DeviceLinkingUtilities.hasValidSlaveSignature(deviceLink) else { - print("[Loki] Received a device link with an invalid slave signature.") - return nil - } - return deviceLink - } - }) + return handleRawResponseForDeviceLinks(rawResponse: json, data: data) }.map2 { deviceLinks in storage.setDeviceLinks(deviceLinks) return deviceLinks @@ -109,7 +126,10 @@ public final class FileServerAPI : DotNetAPI { let request = TSRequest(url: url, method: "PATCH", parameters: parameters) request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ] return attempt(maxRetryCount: 8, recoveringOn: SnodeAPI.workQueue) { - LokiFileServerProxy(for: server).perform(request).map2 { _ in } + if (useOnionRequests) { + return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { _ in } + } + return LokiFileServerProxy(for: server).perform(request).map2 { _ in } }.handlingInvalidAuthTokenIfNeeded(for: server).recover2 { error in print("Couldn't update device links due to error: \(error).") throw error @@ -161,6 +181,16 @@ public final class FileServerAPI : DotNetAPI { print("[Loki] Couldn't upload profile picture due to error: \(error).") return Promise(error: error) } + if (useOnionRequests) { + return OnionRequestAPI.sendOnionRequestFileServerDest(request, server: server, using: fileServerPublicKey).map2 { json in + guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { + print("[Loki] Couldn't parse profile picture from: \(json).") + throw DotNetAPIError.parsingFailed + } + UserDefaults.standard[.lastProfilePictureUpload] = Date() + return downloadURL + } + } return LokiFileServerProxy(for: server).performLokiFileServerNSURLRequest(request as NSURLRequest).map2 { responseObject in guard let json = responseObject as? JSON, let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else { print("[Loki] Couldn't parse profile picture from: \(responseObject).") diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift index 6bb38b658..12e1b6aa0 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift @@ -35,14 +35,21 @@ extension OnionRequestAPI { let (promise, seal) = Promise<EncryptionResult>.pending() DispatchQueue.global(qos: .userInitiated).async { do { + // The wrapper is not needed when it is a file server onion request guard JSONSerialization.isValidJSONObject(payload) else { return seal.reject(HTTP.Error.invalidJSON) } - let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) - let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string - let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ] - guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) } - let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ]) - let result = try encrypt(plaintext, using: x25519Key) - seal.fulfill(result) + if let destination = destination["destination"] { + let payloadAsData = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) + let payloadAsString = String(data: payloadAsData, encoding: .utf8)! // Snodes only accept this as a string + let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ] + guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) } + let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ]) + let result = try encrypt(plaintext, using: x25519Key) + seal.fulfill(result) + } else { + let plaintext = try JSONSerialization.data(withJSONObject: payload, options: [ .fragmentsAllowed ]) + let result = try encrypt(plaintext, using: x25519Key) + seal.fulfill(result) + } } catch (let error) { seal.reject(error) } diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index 2423e7830..7c15554f4 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -198,7 +198,7 @@ public enum OnionRequestAPI { // Recursively encrypt the layers of the onion (again in reverse order) encryptionResult = r var path = path - var destination: JSON = [:] + var destination = dest func addLayer() -> Promise<EncryptionResult> { if path.isEmpty { return Promise<EncryptionResult> { $0.fulfill(encryptionResult) } @@ -218,6 +218,17 @@ public enum OnionRequestAPI { } // MARK: Internal API + internal static func getCanonicalHeaders(for request: NSURLRequest) -> [String:Any] { + guard let headers = request.allHTTPHeaderFields else { return [:] } + return headers.mapValues { value in + switch value.lowercased() { + case "true": return true + case "false": return false + default: return value + } + } + } + /// Sends an onion request to `snode`. Builds new paths as needed. internal static func sendOnionRequestSnodeDest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise<JSON> { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] @@ -230,11 +241,39 @@ public enum OnionRequestAPI { } /// Sends an onion request to `file server`. Builds new paths as needed. - internal static func sendOnionRequestLsrpcDest(to host: String, with payload: JSON, using x25519Key: String, associatedWith publicKey: String) -> Promise<JSON> { - let destination: JSON = [ "host" : host, + internal static func sendOnionRequestFileServerDest(_ request: NSURLRequest, server: String, using x25519Key: String) -> Promise<JSON> { + var headers = getCanonicalHeaders(for: request) + let urlAsString = request.url!.absoluteString + let serverURLEndIndex = urlAsString.range(of: server)!.upperBound + let endpointStartIndex = urlAsString.index(after: serverURLEndIndex) + let endpoint = String(urlAsString[endpointStartIndex..<urlAsString.endIndex]) + let parametersAsString: String + if let tsRequest = request as? TSRequest { + headers["Content-Type"] = "application/json" + let parametersAsData = try! JSONSerialization.data(withJSONObject: tsRequest.parameters, options: [ .fragmentsAllowed ]) + parametersAsString = !tsRequest.parameters.isEmpty ? String(bytes: parametersAsData, encoding: .utf8)! : "null" + } else { + headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] + if let parametersAsInputStream = request.httpBodyStream, let parametersAsData = try? Data(from: parametersAsInputStream) { + parametersAsString = "{ \"fileUpload\" : \"\(String(data: parametersAsData.base64EncodedData(), encoding: .utf8) ?? "null")\" }" + } else { + parametersAsString = "null" + } + } + let payload: JSON = [ + "body" : parametersAsString, + "endpoint": endpoint, + "method" : request.httpMethod, + "headers" : headers + ] + let destination: JSON = [ "host" : request.url?.host, "target" : "/loki/v1/lsrpc", "method" : "POST"] - let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: publicKey) + let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: getUserHexEncodedPublicKey()) + promise.recover2{ error -> Promise<JSON> in + // TODO: File Server API handle Error + throw error + } return promise }