diff --git a/SignalServiceKit/src/Loki/API/FileServerAPI.swift b/SignalServiceKit/src/Loki/API/FileServerAPI.swift index bcc4f83ad..3a0b4e0de 100644 --- a/SignalServiceKit/src/Loki/API/FileServerAPI.swift +++ b/SignalServiceKit/src/Loki/API/FileServerAPI.swift @@ -9,6 +9,8 @@ public final class FileServerAPI : DotNetAPI { public static let maxFileSize = 10_000_000 // 10 MB @objc public static let server = "https://file.getsession.org" + + internal static var useOnionRequests = true // MARK: Storage override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift index 731ba4959..6bb38b658 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI+Encryption.swift @@ -18,12 +18,12 @@ extension OnionRequestAPI { } /// - Note: Sync. Don't call from the main thread. - private static func encrypt(_ plaintext: Data, forSnode snode: Snode) throws -> EncryptionResult { + private static func encrypt(_ plaintext: Data, using x25519Key: String?) throws -> EncryptionResult { guard !Thread.isMainThread else { preconditionFailure("It's illegal to call encrypt(_:forSnode:) from the main thread.") } - guard let hexEncodedSnodeX25519PublicKey = snode.publicKeySet?.x25519Key else { throw Error.snodePublicKeySetMissing } - let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey) + guard let hexEncodedX25519PublicKey = x25519Key else { throw Error.snodePublicKeySetMissing } + let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey) let ephemeralKeyPair = Curve25519.generateKeyPair() - let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: snodeX25519PublicKey, privateKey: ephemeralKeyPair.privateKey) + let ephemeralSharedSecret = try Curve25519.generateSharedSecret(fromPublicKey: x25519PublicKey, privateKey: ephemeralKeyPair.privateKey) let salt = "LOKI" let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes) let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey)) @@ -31,7 +31,7 @@ extension OnionRequestAPI { } /// Encrypts `payload` for `snode` and returns the result. Use this to build the core of an onion request. - internal static func encrypt(_ payload: JSON, forTargetSnode snode: Snode) -> Promise { + internal static func encrypt(_ payload: JSON, using x25519Key: String?, to destination: JSON) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { do { @@ -41,7 +41,7 @@ extension OnionRequestAPI { 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, forSnode: snode) + let result = try encrypt(plaintext, using: x25519Key) seal.fulfill(result) } catch (let error) { seal.reject(error) @@ -51,18 +51,16 @@ extension OnionRequestAPI { } /// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request. - internal static func encryptHop(from lhs: Snode, to rhs: Snode, using previousEncryptionResult: EncryptionResult) -> Promise { + internal static func encryptHop(with x25519Key: String?, to destination: JSON, using previousEncryptionResult: EncryptionResult) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { - let parameters: JSON = [ - "ciphertext" : previousEncryptionResult.ciphertext.base64EncodedString(), - "ephemeral_key" : previousEncryptionResult.ephemeralPublicKey.toHexString(), - "destination" : rhs.publicKeySet!.ed25519Key - ] + var parameters = destination + parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString() + parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() do { guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) - let result = try encrypt(plaintext, forSnode: lhs) + 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 2be0718e8..ca4470aeb 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -44,6 +44,15 @@ public enum OnionRequestAPI { // MARK: Onion Building Result private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data) + + // MARK: File Server + private static let fileServerPublicKey: Data = { + let base64EncodedPublicKey = "BWJQnVm97sQE3Q1InB4Vuo+U/T1hmwHBv0ipkiv8tzEc" + let publicKeyWithPrefix = Data(base64Encoded: base64EncodedPublicKey)! + let hexEncodedPublicKeyWithPrefix = publicKeyWithPrefix.toHexString() + let hexEncodedPublicKey = hexEncodedPublicKeyWithPrefix.removing05PrefixIfNeeded() + return Data(hex: hexEncodedPublicKey) + }() // MARK: Private API /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. @@ -137,7 +146,7 @@ public enum OnionRequestAPI { /// Returns a `Path` to be used for building an onion request. Builds new paths as needed. /// /// - Note: Exposed for testing purposes. - internal static func getPath(excluding snode: Snode) -> Promise { + internal static func getPath(excluding snode: Snode?) -> Promise { guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } if paths.count < pathCount { let storage = OWSPrimaryStorage.shared() @@ -151,11 +160,19 @@ public enum OnionRequestAPI { // randomElement() uses the system's default random generator, which is cryptographically secure if paths.count >= pathCount { return Promise { seal in - seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) + if let snode = snode { + seal.fulfill(paths.filter { !$0.contains(snode) }.randomElement()!) + } else { + seal.fulfill(paths.randomElement()!) + } } } else { return buildPaths().map2 { paths in - return paths.filter { !$0.contains(snode) }.randomElement()! + if let snode = snode { + return paths.filter { !$0.contains(snode) }.randomElement()! + } else { + return paths.randomElement()! + } } } } @@ -172,27 +189,34 @@ public enum OnionRequestAPI { } /// Builds an onion around `payload` and returns the result. - private static func buildOnion(around payload: JSON, targetedAt snode: Snode) -> Promise { + private static func buildOnion(around payload: JSON, targetedAt snode: Snode?, to destination: JSON = [:], using x25519Key: String? = nil) -> Promise { var guardSnode: Snode! var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode var encryptionResult: EncryptionResult! return getPath(excluding: snode).then2 { path -> Promise in guardSnode = path.first! // Encrypt in reverse order, i.e. the target snode first - return encrypt(payload, forTargetSnode: snode).then2 { r -> Promise in + var dest = destination + var x25519PublicKey = x25519Key + if let snode = snode { + dest = [ "destination": snode.publicKeySet!.ed25519Key ] + x25519PublicKey = snode.publicKeySet?.x25519Key + } + return encrypt(payload, using: x25519PublicKey, to: dest).then2 { r -> Promise in targetSnodeSymmetricKey = r.symmetricKey // Recursively encrypt the layers of the onion (again in reverse order) encryptionResult = r var path = path - var rhs = snode + var destination: JSON = [:] func addLayer() -> Promise { if path.isEmpty { return Promise { $0.fulfill(encryptionResult) } } else { let lhs = path.removeLast() - return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise in + let x25519Key = lhs.publicKeySet?.x25519Key + return OnionRequestAPI.encryptHop(with: x25519Key, to: destination, using: encryptionResult).then2 { r -> Promise in encryptionResult = r - rhs = lhs + destination = [ "destination": lhs.publicKeySet!.ed25519Key ] return addLayer() } } @@ -204,12 +228,30 @@ public enum OnionRequestAPI { // MARK: Internal API /// Sends an onion request to `snode`. Builds new paths as needed. - internal static func sendOnionRequest(invoking method: Snode.Method, on snode: Snode, with parameters: JSON, associatedWith publicKey: String) -> Promise { + 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 ] + let promise = sendOnionRequest(on: snode, with: payload, to: [:], using: nil, associatedWith: publicKey) + promise.recover2 { error -> Promise in + guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error } + throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error + } + return promise + } + + /// 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, + "target" : "/loki/v1/lsrpc", + "method" : "POST"] + let promise = sendOnionRequest(on: nil, with: payload, to: destination, using: x25519Key, associatedWith: publicKey) + return promise + } + + internal static func sendOnionRequest(on snode: Snode?, with payload: JSON, to destination: JSON, using x25519Key: String?, associatedWith publicKey: String) -> Promise { let (promise, seal) = Promise.pending() var guardSnode: Snode! DispatchQueue.global(qos: .userInitiated).async { - let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] - buildOnion(around: payload, targetedAt: snode).done2 { intermediate in + buildOnion(around: payload, targetedAt: snode, to: destination, using: x25519Key).done2 { intermediate in guardSnode = intermediate.guardSnode let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let finalEncryptionResult = intermediate.finalEncryptionResult @@ -254,10 +296,6 @@ public enum OnionRequestAPI { dropAllPaths() // A snode in the path is bad; retry with a different path dropGuardSnode(guardSnode) } - promise.recover2 { error -> Promise in - guard case OnionRequestAPI.Error.httpRequestFailedAtTargetSnode(let statusCode, let json) = error else { throw error } - throw SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode, associatedWith: publicKey) ?? error - } return promise } } diff --git a/SignalServiceKit/src/Loki/API/SnodeAPI.swift b/SignalServiceKit/src/Loki/API/SnodeAPI.swift index b8fd953d0..d4c854ca2 100644 --- a/SignalServiceKit/src/Loki/API/SnodeAPI.swift +++ b/SignalServiceKit/src/Loki/API/SnodeAPI.swift @@ -47,7 +47,7 @@ public final class SnodeAPI : NSObject { // MARK: Core internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { if useOnionRequests { - return OnionRequestAPI.sendOnionRequest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any } + return OnionRequestAPI.sendOnionRequestSnodeDest(invoking: method, on: snode, with: parameters, associatedWith: publicKey).map2 { $0 as Any } } else { let url = "\(snode.address):\(snode.port)/storage_rpc/v1" return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise in