refactor and prepare for file server onion request

This commit is contained in:
ryanzhao 2020-07-17 17:18:26 +10:00
parent e1f6da05e3
commit ff9341b573
4 changed files with 67 additions and 29 deletions

View File

@ -9,6 +9,8 @@ public final class FileServerAPI : DotNetAPI {
public static let maxFileSize = 10_000_000 // 10 MB public static let maxFileSize = 10_000_000 // 10 MB
@objc public static let server = "https://file.getsession.org" @objc public static let server = "https://file.getsession.org"
internal static var useOnionRequests = true
// MARK: Storage // MARK: Storage
override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" } override internal class var authTokenCollection: String { return "LokiStorageAuthTokenCollection" }

View File

@ -18,12 +18,12 @@ extension OnionRequestAPI {
} }
/// - Note: Sync. Don't call from the main thread. /// - 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 !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 } guard let hexEncodedX25519PublicKey = x25519Key else { throw Error.snodePublicKeySetMissing }
let snodeX25519PublicKey = Data(hex: hexEncodedSnodeX25519PublicKey) let x25519PublicKey = Data(hex: hexEncodedX25519PublicKey)
let ephemeralKeyPair = Curve25519.generateKeyPair() 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 salt = "LOKI"
let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes) let symmetricKey = try HMAC(key: salt.bytes, variant: .sha256).authenticate(ephemeralSharedSecret.bytes)
let ciphertext = try encrypt(plaintext, usingAESGCMWithSymmetricKey: Data(bytes: symmetricKey)) 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. /// 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<EncryptionResult> { internal static func encrypt(_ payload: JSON, using x25519Key: String?, to destination: JSON) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending() let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
do { do {
@ -41,7 +41,7 @@ extension OnionRequestAPI {
let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ] let wrapper: JSON = [ "body" : payloadAsString, "headers" : "" ]
guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) } guard JSONSerialization.isValidJSONObject(wrapper) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: wrapper, options: [ .fragmentsAllowed ]) 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) seal.fulfill(result)
} catch (let error) { } catch (let error) {
seal.reject(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. /// 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<EncryptionResult> { internal static func encryptHop(with x25519Key: String?, to destination: JSON, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.pending() let (promise, seal) = Promise<EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let parameters: JSON = [ var parameters = destination
"ciphertext" : previousEncryptionResult.ciphertext.base64EncodedString(), parameters["ciphertext"] = previousEncryptionResult.ciphertext.base64EncodedString()
"ephemeral_key" : previousEncryptionResult.ephemeralPublicKey.toHexString(), parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
"destination" : rhs.publicKeySet!.ed25519Key
]
do { do {
guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) } guard JSONSerialization.isValidJSONObject(parameters) else { return seal.reject(HTTP.Error.invalidJSON) }
let plaintext = try JSONSerialization.data(withJSONObject: parameters, options: [ .fragmentsAllowed ]) 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) seal.fulfill(result)
} catch (let error) { } catch (let error) {
seal.reject(error) seal.reject(error)

View File

@ -44,6 +44,15 @@ public enum OnionRequestAPI {
// MARK: Onion Building Result // MARK: Onion Building Result
private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data) 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 // MARK: Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. /// 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. /// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
/// ///
/// - Note: Exposed for testing purposes. /// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: Snode) -> Promise<Path> { internal static func getPath(excluding snode: Snode?) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
if paths.count < pathCount { if paths.count < pathCount {
let storage = OWSPrimaryStorage.shared() let storage = OWSPrimaryStorage.shared()
@ -151,11 +160,19 @@ public enum OnionRequestAPI {
// randomElement() uses the system's default random generator, which is cryptographically secure // randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= pathCount { if paths.count >= pathCount {
return Promise<Path> { seal in return Promise<Path> { 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 { } else {
return buildPaths().map2 { paths in 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. /// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: JSON, targetedAt snode: Snode) -> Promise<OnionBuildingResult> { private static func buildOnion(around payload: JSON, targetedAt snode: Snode?, to destination: JSON = [:], using x25519Key: String? = nil) -> Promise<OnionBuildingResult> {
var guardSnode: Snode! var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the target snode
var encryptionResult: EncryptionResult! var encryptionResult: EncryptionResult!
return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in return getPath(excluding: snode).then2 { path -> Promise<EncryptionResult> in
guardSnode = path.first! guardSnode = path.first!
// Encrypt in reverse order, i.e. the target snode first // Encrypt in reverse order, i.e. the target snode first
return encrypt(payload, forTargetSnode: snode).then2 { r -> Promise<EncryptionResult> 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<EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order) // Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r encryptionResult = r
var path = path var path = path
var rhs = snode var destination: JSON = [:]
func addLayer() -> Promise<EncryptionResult> { func addLayer() -> Promise<EncryptionResult> {
if path.isEmpty { if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) } return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else { } else {
let lhs = path.removeLast() let lhs = path.removeLast()
return OnionRequestAPI.encryptHop(from: lhs, to: rhs, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in let x25519Key = lhs.publicKeySet?.x25519Key
return OnionRequestAPI.encryptHop(with: x25519Key, to: destination, using: encryptionResult).then2 { r -> Promise<EncryptionResult> in
encryptionResult = r encryptionResult = r
rhs = lhs destination = [ "destination": lhs.publicKeySet!.ed25519Key ]
return addLayer() return addLayer()
} }
} }
@ -204,12 +228,30 @@ public enum OnionRequestAPI {
// MARK: Internal API // MARK: Internal API
/// Sends an onion request to `snode`. Builds new paths as needed. /// 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<JSON> { 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 ]
let promise = sendOnionRequest(on: snode, with: payload, to: [:], using: nil, associatedWith: publicKey)
promise.recover2 { error -> Promise<JSON> 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<JSON> {
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<JSON> {
let (promise, seal) = Promise<JSON>.pending() let (promise, seal) = Promise<JSON>.pending()
var guardSnode: Snode! var guardSnode: Snode!
DispatchQueue.global(qos: .userInitiated).async { DispatchQueue.global(qos: .userInitiated).async {
let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] buildOnion(around: payload, targetedAt: snode, to: destination, using: x25519Key).done2 { intermediate in
buildOnion(around: payload, targetedAt: snode).done2 { intermediate in
guardSnode = intermediate.guardSnode guardSnode = intermediate.guardSnode
let url = "\(guardSnode.address):\(guardSnode.port)/onion_req" let url = "\(guardSnode.address):\(guardSnode.port)/onion_req"
let finalEncryptionResult = intermediate.finalEncryptionResult let finalEncryptionResult = intermediate.finalEncryptionResult
@ -254,10 +296,6 @@ public enum OnionRequestAPI {
dropAllPaths() // A snode in the path is bad; retry with a different path dropAllPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode) dropGuardSnode(guardSnode)
} }
promise.recover2 { error -> Promise<JSON> 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 return promise
} }
} }

View File

@ -47,7 +47,7 @@ public final class SnodeAPI : NSObject {
// MARK: Core // MARK: Core
internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise { internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String, parameters: JSON) -> RawResponsePromise {
if useOnionRequests { 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 { } else {
let url = "\(snode.address):\(snode.port)/storage_rpc/v1" let url = "\(snode.address):\(snode.port)/storage_rpc/v1"
return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in return HTTP.execute(.post, url, parameters: parameters).map2 { $0 as Any }.recover2 { error -> Promise<Any> in