From b927a9596fdaf63a42bf58f9bba2899bca1f34b7 Mon Sep 17 00:00:00 2001 From: nielsandriesse Date: Tue, 6 Oct 2020 16:52:31 +1100 Subject: [PATCH] Implement new onion request encoding --- .../OnionRequestAPI+Encryption.swift | 13 ++++++++++--- .../API/Onion Requests/OnionRequestAPI.swift | 13 +++++++++---- .../src/Loki/API/Utilities/HTTP.swift | 19 +++++++++++++++---- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift index a56a0bab8..11f80c6c9 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift @@ -3,6 +3,15 @@ import PromiseKit extension OnionRequestAPI { + internal static func encode(ciphertext: Data, json: JSON) throws -> Data { + // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | + guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON } + let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ]) + let ciphertextSize = Int32(ciphertext.count) + let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: 4) } + return ciphertextSizeAsData + ciphertext + jsonAsData + } + /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. internal static func encrypt(_ payload: JSON, for destination: Destination) -> Promise { let (promise, seal) = Promise.pending() @@ -44,7 +53,6 @@ extension OnionRequestAPI { case .server(let host, _): parameters = [ "host" : host, "target" : "/loki/v1/lsrpc", "method" : "POST" ] } - parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString() parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() let x25519PublicKey: String switch lhs { @@ -55,8 +63,7 @@ extension OnionRequestAPI { x25519PublicKey = serverX25519PublicKey } do { - guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } - let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) + let plaintext = try encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters) let result = try EncryptionUtilities.encrypt(plaintext, using: x25519PublicKey) seal.fulfill(result) } catch (let error) { diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index c4188cbc0..dc1873cd0 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -56,7 +56,7 @@ public enum OnionRequestAPI { let timeout: TimeInterval = 3 // Use a shorter timeout for testing HTTP.execute(.get, url, timeout: timeout).done2 { rawResponse in guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } - if version >= "2.0.0" { + if version >= "2.0.7" { seal.fulfill(()) } else { print("[Loki] [Onion Request API] Unsupported snode version: \(version).") @@ -282,18 +282,23 @@ public enum OnionRequestAPI { SnodeAPI.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` buildOnion(around: payload, targetedAt: destination).done2 { intermediate in guardSnode = intermediate.guardSnode - let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" + let url = "\(guardSnode.address):\(guardSnode.port)/onion_req/v2" let finalEncryptionResult = intermediate.finalEncryptionResult let onion = finalEncryptionResult.ciphertext if case Destination.server = destination, Double(onion.count) > 0.75 * Double(FileServerAPI.maxFileSize) { print("[Loki] Approaching request size limit: ~\(onion.count) bytes.") } let parameters: JSON = [ - "ciphertext" : onion.base64EncodedString(), "ephemeral_key" : finalEncryptionResult.ephemeralPublicKey.toHexString() ] + let body: Data + do { + body = try encode(ciphertext: onion, json: parameters) + } catch { + return seal.reject(error) + } let destinationSymmetricKey = intermediate.destinationSymmetricKey - HTTP.execute(.post, url, parameters: parameters).done2 { rawResponse in + HTTP.execute(.post, url, body: body).done2 { rawResponse in guard let json = rawResponse as? JSON, let base64EncodedIVAndCiphertext = json["result"] as? String, let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= EncryptionUtilities.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } do { diff --git a/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift b/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift index ccc0744fc..c940fc53f 100644 --- a/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift +++ b/SignalServiceKit/src/Loki/API/Utilities/HTTP.swift @@ -41,17 +41,28 @@ public enum HTTP { } // MARK: Main - public static func execute(_ verb: Verb, _ url: String, parameters: JSON? = nil, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { - var request = URLRequest(url: URL(string: url)!) - request.httpMethod = verb.rawValue + public static func execute(_ verb: Verb, _ url: String, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { + return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) + } + + public static func execute(_ verb: Verb, _ url: String, parameters: JSON?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { if let parameters = parameters { do { guard JSONSerialization.isValidJSONObject(parameters) else { return Promise(error: Error.invalidJSON) } - request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) + let body = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) + return execute(verb, url, body: body, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) } catch (let error) { return Promise(error: error) } + } else { + return execute(verb, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession) } + } + + public static func execute(_ verb: Verb, _ url: String, body: Data?, timeout: TimeInterval = HTTP.timeout, useSeedNodeURLSession: Bool = false) -> Promise { + var request = URLRequest(url: URL(string: url)!) + request.httpMethod = verb.rawValue + request.httpBody = body request.timeoutInterval = timeout let (promise, seal) = Promise.pending() let urlSession = useSeedNodeURLSession ? seedNodeURLSession : defaultURLSession