patbef-iOS/Befund/Core/Https/Request.swift

446 lines
23 KiB
Swift

//
// 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
}
}
}
}