// // Request.swift // Befund // // Created by Irakli Abetschkhrischwili on 15.05.22. // Copyright © 2022 MVZ Dr. Stein und Kollegen. All rights reserved. import Foundation import UIKit import Combine import CryptoKit extension Core { public struct Https { public class Request { /** * Low Level Header protector */ private static let HeaderSecureHash: String = "3HzKQw/&>*VH^?cAR{Qd5pP~6w(V5%aZ"; private static let HeaderValueSecureHash: String = "7*R3P!f:sC4Q8XS:@*J/z:4/sS;V3GnM"; /** * Sends Key Exchange Request to the server in async mode and calls callback(onSucces or onError) * @param host - Servers.Host that should get this request * @param keyPair - Client keyPair(if not given it will create a new one) * @param onSuccess - onSuccess Handler * @param onError - onError Handler */ static func KeyExchangeAsync(host: Servers, keyPair: Core.Security.Curve25519.KeyPair? = nil, onSuccess: ((Core.Models.Response.PublicKey)->Void)? = nil, onError: ((String)->Void)? = nil) { PostKeyExchangeRequestAsync().RunAsync(host: host, controller: "exchange", action: "key", _keyPair: keyPair, onSuccess: onSuccess, onError: onError); } /** * Sends Encrypted Request to the server in async mode and calls callback(onSucces or onError) * @param host - Servers.Host that should get this request * @param controller - controller of the request * @param action - controller action of the request * @param request - Encrypted Request that should be sent to the server * @param serverPublicKey - Server Public Key from Exchange Request * @param keyPair - Client KeyPair (if not given it will create a new one) * @param onSuccess - onSuccess Handler * @param onError - onError Handler */ public static func EncryptedRequestAsync(host: Servers, controller: String, action: String, request: Core.Models.Request.EncryptedRequest, serverPublicKey: Core.Models.Response.PublicKey, keyPair: Core.Security.Curve25519.KeyPair? = nil, onSuccess: ((Core.Models.Response.EncryptedResponse)->Void)? = nil, onError: ((String)->Void)? = nil) { PostEncryptedRequestAsync().RunAsync(host: host, controller: controller, action: action, _request: request, serverPublicKey:serverPublicKey, _keyPair: keyPair, onSuccess: onSuccess, onError: onError); } /** * Sends EncryptedRequest to the server in async mode and calls callback(onSucces or onError) */ public class PostEncryptedRequestAsync: NSObject, URLSessionDelegate { /** * Runs encrypted request async and calls ether onSuccess or onError callback * * @param host - host of the server s. Servers enum * @param controller - controller of request * @param action - action of request * @param _request - encrypted request that contains descriptor, request object and hmac * @param serverPublicKey - Exchanged server public key from the first request * @param _keyPair - Curve25519 KeyPair (Private & Public Keys) * @param onSuccess - onSuccess callback that contains public key of response(Server) * @param onError - onError callback that contains error message */ func RunAsync(host: Servers, controller: String, action: String, _request: Core.Models.Request.EncryptedRequest, serverPublicKey: Core.Models.Response.PublicKey, _keyPair: Core.Security.Curve25519.KeyPair? = nil, onSuccess: ((Core.Models.Response.EncryptedResponse)->Void)? = nil, onError: ((String)->Void)? = nil) { var keyPair = _keyPair; do { if(keyPair == nil) { keyPair = Security.Curve25519.GenerateKeyPair() } let strURL = Core.Https.Request.wellFormedControllerAction(host: host, controller: controller, action: action) let url = URL(string: strURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(host.rawValue, forHTTPHeaderField: "Host") request.setValue("BefundApp", forHTTPHeaderField: "User-Agent") request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(Security.AES.Encrypt(value: HeaderValueSecureHash, deriveKey: SymmetricKey(data: Security.AES.GetKey(password: HeaderSecureHash))), forHTTPHeaderField: "Client-Hash") request.setValue(keyPair!.PublicKey, forHTTPHeaderField: "Client-Key") request.setValue(keyPair!.SigningPublicKey, forHTTPHeaderField: "Client-Signature-Key") request.setValue(keyPair!.GetSignature(signingValue: (_request.encrypted_content != nil ? _request.encrypted_content : _request.descriptor)!), forHTTPHeaderField: "Client-Signature") request.httpBody = try JSONEncoder().encode(_request) let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) DispatchQueue.global(qos: .background).async { let task = session.dataTask(with: request) { if($2 != nil) { self.responseError(message: $2!.localizedDescription, callback: onError) Log.Error(err: $2!, namespace: "Core.Https.Request.PostEncryptedRequestAsync", method: "RunAsync") } if($1 != nil) { let httpUrlResponse = $1 as? HTTPURLResponse if(httpUrlResponse != nil) { if(httpUrlResponse!.statusCode == 429) { AppDelegate.Session.Maintenance = false AppDelegate.Session.MaintenanceNotified = false self.responseError(message: Core.Lang.Get(key: "ERROR_TOO_MANY_REQUESTS"), callback: onError) } else if (httpUrlResponse!.statusCode == 503) { AppDelegate.Session.Maintenance = true self.responseError(message: Core.Lang.Get(key: "MSG_MAINTENANCE"), callback: onError) } else { AppDelegate.Session.Maintenance = false AppDelegate.Session.MaintenanceNotified = false if(!Core.Https.Request.validServerHeaders(server: httpUrlResponse!)) { self.responseError(message: Core.Lang.Get(key: "ERROR_INVALID_RESPONSE"), callback: onError) } else { if($0 != nil) { self.responseEncryptedResponse(data: $0!, server: httpUrlResponse!, deriveKey: keyPair!.GetSharedKey(peerPublicKeyBase64: serverPublicKey.key!)!, callback: onSuccess) } else { self.responseError(message: Core.Lang.Get(key: "ERROR_INVALID_RESPONSE"), callback: onError) } } } } } } task.resume() } } catch { Log.Error(err: error, namespace: "Core.Https.Request.PostKeyExchangeRequestAsync", method: "RunAsync") } } /** * Calls OnError Handler with a error message * * @param message - error message * @callback - callback of error response */ private func responseError(message: String, callback: ((String)->Void)? = nil) { if(callback != nil) { callback!(message) } } /** * Calls OnSuccess Handler with a EncryptedResponse * @param data - response data * @param server - server context that contains response header * @param deriveKey - deriveKey * @param callback - onSuccess callback */ private func responseEncryptedResponse(data: Data, server: HTTPURLResponse, deriveKey: SymmetricKey, callback: ((Core.Models.Response.EncryptedResponse)->Void)? = nil) { if(callback != nil) { do { let encryptedResponse = try JSONDecoder().decode(Core.Models.Response.EncryptedResponse.self, from: data) if(Core.Https.Request.validServerSignature(server: server, encryptedResponse: encryptedResponse, deriveKey: deriveKey)) { callback!(encryptedResponse) } } catch { Log.Error(err: error, namespace: "Core.Https.Request.PostKeyExchangeRequestAsync", method: "responseEncryptedResponse(data, Callback") } } } public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { #if DEBUG completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) #else if(challenge.protectionSpace.host.contains("pba-simulator.patientenbefundapp.labor-limbach-hannover.de")) { completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) } else { completionHandler(.performDefaultHandling, nil) } #endif } } /** * Sends Key Exchange Post request to the server in async mode and calls callback(onSucces or onError) */ public class PostKeyExchangeRequestAsync: NSObject, URLSessionDelegate { /** * Runs request async and calls ether onSuccess or onError callback * * @param host - host of the server s. Servers enum * @param controller - controller of request * @param action - action of request * @param _keyPair - Curve25519 KeyPair (Private & Public Keys) * @param onSuccess - onSuccess callback that contains public key of response(Server) * @param onError - onError callback that contains error message */ func RunAsync(host: Servers, controller: String, action: String, _keyPair: Core.Security.Curve25519.KeyPair? = nil, onSuccess: ((Core.Models.Response.PublicKey)->Void)? = nil, onError: ((String)->Void)? = nil) { var keyPair = _keyPair; do { if(keyPair == nil) { keyPair = Security.Curve25519.GenerateKeyPair() } let strURL = Core.Https.Request.wellFormedControllerAction(host: host, controller: controller, action: action) let url = URL(string: strURL)! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue(host.rawValue, forHTTPHeaderField: "Host"); request.setValue("BefundApp", forHTTPHeaderField: "User-Agent"); request.setValue("application/json", forHTTPHeaderField: "Content-Type"); request.setValue(Security.AES.Encrypt(value: HeaderValueSecureHash, deriveKey: SymmetricKey(data: Security.AES.GetKey(password: HeaderSecureHash))), forHTTPHeaderField: "Client-Hash"); request.setValue(keyPair!.PublicKey, forHTTPHeaderField: "Client-Key"); request.httpBody = try JSONEncoder().encode(Core.Models.Request.KeyExchange()) let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) DispatchQueue.global(qos: .background).async { let task = session.dataTask(with: request) { if($2 != nil) { self.responseError(message: $2!.localizedDescription, callback: onError) Log.Error(err: $2!, namespace: "Core.Https.Request.PostKeyExchangeRequestAsync", method: "RunAsync") } if($1 != nil) { let httpUrlResponse = $1 as? HTTPURLResponse if(httpUrlResponse != nil) { if(httpUrlResponse!.statusCode == 429) { AppDelegate.Session.Maintenance = false AppDelegate.Session.MaintenanceNotified = false self.responseError(message: Core.Lang.Get(key: "ERROR_TOO_MANY_REQUESTS"), callback: onError) } else if (httpUrlResponse!.statusCode == 503) { AppDelegate.Session.Maintenance = true self.responseError(message: Core.Lang.Get(key: "MSG_MAINTENANCE"), callback: onError) } else { AppDelegate.Session.Maintenance = false AppDelegate.Session.MaintenanceNotified = false if(!Core.Https.Request.validServerHeaders(server: httpUrlResponse!)) { self.responseError(message: Core.Lang.Get(key: "ERROR_INVALID_RESPONSE"), callback: onError) } else { if($0 != nil) { self.responsePublicKey(data: $0!, callback: onSuccess) } else { self.responseError(message: Core.Lang.Get(key: "ERROR_INVALID_RESPONSE"), callback: onError) } } } } } } task.resume() } } catch { Log.Error(err: error, namespace: "Core.Https.Request.PostKeyExchangeRequestAsync", method: "RunAsync") } } /** * Calls OnError Handler with a error message * * @param message - error message * @callback - callback of error response */ private func responseError(message: String, callback: ((String)->Void)? = nil) { if(callback != nil) { callback!(message) } } /** * Calls OnSuccess Handler with a PublicKey * @param publicKey - Public Key of the server */ private func responsePublicKey(data: Data, callback: ((Core.Models.Response.PublicKey)->Void)? = nil) { if(callback != nil) { do { let publicKey = try JSONDecoder().decode(Core.Models.Response.PublicKey.self, from: data) callback!(publicKey) } catch { Log.Error(err: error, namespace: "Core.Https.Request.PostKeyExchangeRequestAsync", method: "responsePublicKey(data, Callback") } } } public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { #if DEBUG // Accept all certificates let urlCredential = URLCredential(trust: challenge.protectionSpace.serverTrust!) completionHandler(.useCredential, urlCredential) #else completionHandler(.performDefaultHandling, nil) #endif } } /** * Forms a controller action for the request * @param host - Host of URL * @param controller - Controller of request * @param action - Action of Controller, that should be called * @return returns well formed url for the request */ public static func wellFormedControllerAction(host: Servers, controller: String?, action: String?) -> String { var result: String = "https://" + host.rawValue; if(controller != nil && !controller!.isEmpty) { result = result + "/" + controller! } if(action != nil && !action!.isEmpty) { result = result + "/" + action! } return result } /** * Verifies server header * @param server - server that sends response to the client * @return returns true if server header and signature are valid */ public static func validServerHeaders(server: HTTPURLResponse) -> Bool { var result: Bool = false let userAgent = server.value(forHTTPHeaderField: "User-Agent") let contentType = server.value(forHTTPHeaderField: "Content-Type") let serverHash = server.value(forHTTPHeaderField: "Server-Hash") let serverKey = server.value(forHTTPHeaderField: "Server-Key") if(userAgent != nil && userAgent == "BefundAppServer" && contentType != nil && contentType == "application/json" && serverHash != nil && HeaderValueSecureHash == Security.AES.Decrypt(value: serverHash!, deriveKey: SymmetricKey(data: Security.AES.GetKey(password: HeaderSecureHash))) && serverKey != nil) { result = true; } return result } /** * Validates the server response by signature and HMAC * * @param server - server that sends response to the client * @param encryptedResponse - encrypted response with hmac * @param deriveKey - derive key with server * @return returns true if valid */ public static func validServerSignature(server: HTTPURLResponse, encryptedResponse: Core.Models.Response.EncryptedResponse, deriveKey: SymmetricKey) -> Bool { var result: Bool = false let signature = server.value(forHTTPHeaderField: "Server-Signature") let signatureKey = server.value(forHTTPHeaderField: "Server-Signature-Key") if(signature != nil && !signature!.isEmpty && signatureKey != nil && !signatureKey!.isEmpty && encryptedResponse.ValidSignature(deriveKey: deriveKey, clientSignature: signature!, clientSignatureKey: signatureKey!)) { result = true; } return result } } } }