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) -> Promise> { let hexEncodedPublicKeysDescription = "[ \(hexEncodedPublicKeys.joined(separator: ", ")) ]" print("[Loki] Getting device links for: \(hexEncodedPublicKeysDescription).") + + func handleRawResponseForDeviceLinks(rawResponse: JSON, data: [JSON]) -> Set { + 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> 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 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 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.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 { if path.isEmpty { return Promise { $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 { 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 { - let destination: JSON = [ "host" : host, + internal static func sendOnionRequestFileServerDest(_ request: NSURLRequest, server: String, using x25519Key: String) -> Promise { + 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.. Promise in + // TODO: File Server API handle Error + throw error + } return promise }