// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // // stringlint:disable import Foundation import Combine import CryptoKit import SessionUtilitiesKit internal extension OnionRequestAPI { static func encode(ciphertext: Data, json: JSON) -> AnyPublisher { // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 | guard JSONSerialization.isValidJSONObject(json), let jsonAsData = try? JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ]) else { return Fail(error: HTTPError.invalidJSON) .eraseToAnyPublisher() } let ciphertextSize = Int32(ciphertext.count).littleEndian let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout.size) } return Just(ciphertextSizeAsData + ciphertext + jsonAsData) .setFailureType(to: Error.self) .eraseToAnyPublisher() } /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. static func encrypt( _ payload: Data, for destination: OnionRequestAPIDestination ) -> AnyPublisher { switch destination { case .snode(let snode): // Need to wrap the payload for snode requests return encode(ciphertext: payload, json: [ "headers" : "" ]) .tryMap { data -> AES.GCM.EncryptionResult in try AES.GCM.encrypt(data, for: snode.x25519PublicKey) } .eraseToAnyPublisher() case .server(_, _, let serverX25519PublicKey, _, _): do { return Just(try AES.GCM.encrypt(payload, for: serverX25519PublicKey)) .setFailureType(to: Error.self) .eraseToAnyPublisher() } catch { return Fail(error: error) .eraseToAnyPublisher() } } } /// 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. static func encryptHop( from lhs: OnionRequestAPIDestination, to rhs: OnionRequestAPIDestination, using previousEncryptionResult: AES.GCM.EncryptionResult ) -> AnyPublisher { var parameters: JSON switch rhs { case .snode(let snode): let snodeED25519PublicKey = snode.ed25519PublicKey parameters = [ "destination" : snodeED25519PublicKey ] case .server(let host, let target, _, let scheme, let port): let scheme = scheme ?? "https" let port = port ?? (scheme == "https" ? 443 : 80) parameters = [ "host" : host, "target" : target, "method" : "POST", "protocol" : scheme, "port" : port ] } parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() let x25519PublicKey: String = { switch lhs { case .snode(let snode): return snode.x25519PublicKey case .server(_, _, let serverX25519PublicKey, _, _): return serverX25519PublicKey } }() return encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters) .tryMap { data -> AES.GCM.EncryptionResult in try AES.GCM.encrypt(data, for: x25519PublicKey) } .eraseToAnyPublisher() } }