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
@objc public static let server = "https://file.getsession.org"
internal static var useOnionRequests = true
// MARK: Storage
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.
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<EncryptionResult> {
internal static func encrypt(_ payload: JSON, using x25519Key: String?, to destination: JSON) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.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<EncryptionResult> {
internal static func encryptHop(with x25519Key: String?, to destination: JSON, using previousEncryptionResult: EncryptionResult) -> Promise<EncryptionResult> {
let (promise, seal) = Promise<EncryptionResult>.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)

View File

@ -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<Path> {
internal static func getPath(excluding snode: Snode?) -> Promise<Path> {
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<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 {
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<OnionBuildingResult> {
private static func buildOnion(around payload: JSON, targetedAt snode: Snode?, to destination: JSON = [:], using x25519Key: String? = nil) -> Promise<OnionBuildingResult> {
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<EncryptionResult> in
guardSnode = path.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
// 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<EncryptionResult> {
if path.isEmpty {
return Promise<EncryptionResult> { $0.fulfill(encryptionResult) }
} else {
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
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<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()
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<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
}
}

View File

@ -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<Any> in