diff --git a/FitConnect.sln b/FitConnect.sln index f8c5dc4923d56b8260af2d935548d2b618380858..c4947d83d074b7e6a2996d114be48913a09b8be9 100644 --- a/FitConnect.sln +++ b/FitConnect.sln @@ -2,9 +2,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FitConnect", "FitConnect\FitConnect.csproj", "{DFF6A0D9-5AA1-4640-B26C-4A0A28E42FA1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D382641C-B027-411A-814C-C8C20A9505F3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InitializationTests", "InitializationTests\InitializationTests.csproj", "{73CE5625-4C13-458E-B524-0DAA850F4041}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DummyClient", "DummyClient\DummyClient.csproj", "{DEF51494-6BCD-4441-8D76-6769BBA2C089}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "E2ETests", "E2ETests\E2ETests.csproj", "{27115A99-2AE8-42BC-9495-BE2DCEDDF1E8}" EndProject diff --git a/FitConnect/FunctionalBaseClass.cs b/FitConnect/FunctionalBaseClass.cs new file mode 100644 index 0000000000000000000000000000000000000000..3439ea53278623cbf45597c415b591d350812ce1 --- /dev/null +++ b/FitConnect/FunctionalBaseClass.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using FitConnect.Models; +using Microsoft.Extensions.Logging; + +namespace FitConnect; + +public class FitConnectEndpoints { + /// <summary> + /// Default constructor. + /// </summary> + /// <param name="tokenUrl">URL for receiving the OAuth token</param> + /// <param name="submissionApi">URL for the submission API</param> + /// <param name="routingApi">URL for the routing API</param> + public FitConnectEndpoints(string tokenUrl, string submissionApi, string routingApi) { + TokenUrl = tokenUrl; + SubmissionApi = submissionApi; + RoutingApi = routingApi; + } + + /// <summary> + /// URL for receiving the OAuth token. + /// </summary> + public string TokenUrl { get; } + + /// <summary> + /// URL for the submission API. + /// </summary> + public string SubmissionApi { get; } + + /// <summary> + /// URL for the routing API. + /// </summary> + public string RoutingApi { get; } + + + public enum EndpointType { + Development, + Testing, + Production + } + + /// <summary> + /// Creates the endpoints for the given environment. + /// </summary> + /// <param name="endpointType">Environment to get endpoints for</param> + /// <returns></returns> + /// <exception cref="ArgumentException">Not all environments are ready to use</exception> + public static FitConnectEndpoints Create(EndpointType endpointType) { + return endpointType switch { + EndpointType.Development => DevEndpoints, + EndpointType.Testing => throw new ArgumentException("Not approved for online testing"), + EndpointType.Production => throw new ArgumentException("NOT PRODUCTION READY"), + _ => throw new ArgumentOutOfRangeException(nameof(endpointType), endpointType, null) + }; + } + + private static readonly FitConnectEndpoints DevEndpoints = new( + "https://auth-testing.fit-connect.fitko.dev/token", + "https://submission-api-testing.fit-connect.fitko.dev", + "https://routing-api-testing.fit-connect.fitko.dev" + ); + + private static readonly FitConnectEndpoints TestEndpoints = new( + "https://auth-testing.fit-connect.fitko.dev/token", + "https://submission-api-testing.fit-connect.fitko.dev", + "https://routing-api-testing.fit-connect.fitko.dev" + ); + + private static readonly FitConnectEndpoints ProductionEndpoints = new( + "https://auth-testing.fit-connect.fitko.dev/token", + "https://submission-api-testing.fit-connect.fitko.dev", + "https://routing-api-testing.fit-connect.fitko.dev" + ); +} + +public abstract class FunctionalBaseClass { + protected readonly ILogger? logger; + private RSA _rsa; + public FitConnectEndpoints Endpoints { get; } + + protected FunctionalBaseClass(ILogger? logger, FitConnectEndpoints? endpoints) { + Endpoints = endpoints ?? + FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development); + + this.logger = logger; + } + + + /// <summary> + /// Requesting an OAuth token from the FitConnect API. + /// + /// You can get the Client ID and Client Secret from the FitConnect Self Service portal + /// under https://portal.auth-testing.fit-connect.fitko.dev + /// </summary> + /// <param name="clientId">Your client Id</param> + /// <param name="clientSecret">Your client Secret</param> + /// <param name="scope">Scope if needed</param> + /// <returns></returns> + public async Task<OAuthAccessToken?> GetTokenAsync(string clientId, string clientSecret, + string? scope = null) { + var client = new HttpClient(); + client.DefaultRequestHeaders.Accept.Add( + MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var requestContent = new Dictionary<string, string> { + { "grant_type", "client_credentials" }, + { "client_id", clientId }, + { "client_secret", clientSecret } + }; + + if (scope != null) + requestContent["scope"] = scope; + + var content = new FormUrlEncodedContent(requestContent); + + var request = new HttpRequestMessage(HttpMethod.Post, Endpoints.TokenUrl) { + Content = content, + Method = HttpMethod.Post + }; + + var response = await client.SendAsync(request); + + return await response.Content.ReadFromJsonAsync<OAuthAccessToken>(); + } + + public Task<SecurityEventToken> GetSetDataAsync() { + throw new NotImplementedException(); + } + + public byte[] EncryptDataAsync(byte[] data, + byte[]? publicKey, + string? password, + int numberOfIterations) { + _rsa = RSA.Create(2048); + // _rsa.ImportRSAPublicKey(publicKey, out var read); + + logger?.LogInformation( + "Encrypting data with public key: {}", + Convert.ToBase64String(_rsa.ExportRSAPublicKey())); + + // var keyParams = new PbeParameters( + // PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, numberOfIterations); + // + // var privateKey = + // rsa.ExportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), keyParams); + // + // logger?.LogInformation( + // "Private key: {}", privateKey); + + return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public byte[] DecryptDataAsync(byte[] data, byte[] privateKey) { + return _rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + + private async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.DefaultRequestHeaders.Add("Content-Type", "application/json"); + + var request = new HttpRequestMessage(); + request.Method = method; + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + request.RequestUri = uri; + + var response = await client.SendAsync(request); + + if (response.IsSuccessStatusCode) + return (await response.Content.ReadFromJsonAsync<T>()); + + throw new HttpRequestException("Error calling FitConnect API"); + } +} diff --git a/FitConnect/Interfaces/IBaseFunctionality.cs b/FitConnect/Interfaces/IBaseFunctionality.cs new file mode 100644 index 0000000000000000000000000000000000000000..14665ab9662ba3662f28bca67de41159ec4162c0 --- /dev/null +++ b/FitConnect/Interfaces/IBaseFunctionality.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using FitConnect.Models; + +namespace FitConnect; + +public interface IBaseFunctionality { + Task<OAuthAccessToken> GetTokenAsync(string clientId, string clientSecret, + string? scope = null); + + Task<SecurityEventToken> GetSetDataAsync(); + + // Receive SecurityEventToken and check signature +} \ No newline at end of file diff --git a/FitConnect/Interfaces/ISender.cs b/FitConnect/Interfaces/ISender.cs new file mode 100644 index 0000000000000000000000000000000000000000..ed47036ac5e3501093bb6a180658c06e74e3ffe3 --- /dev/null +++ b/FitConnect/Interfaces/ISender.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace FitConnect; + +public interface ISender : IBaseFunctionality { + // Check public keys + Task<bool> CheckPublicKeyAsync(string publicKey); + + // Encrypt Data (Fachdaten) + byte[] EncryptDataAsync(string data, string publicKey); + + // Encrypt attachments (Anhänge) + Task<string> EncryptAttachmentAsync(string attachment, string publicKey); + + // Create Metadata incl. Hash + Task<string> CreateMetadataAsync(string data, string attachment, string publicKey); +} \ No newline at end of file diff --git a/FitConnect/Interfaces/ISubscriber.cs b/FitConnect/Interfaces/ISubscriber.cs new file mode 100644 index 0000000000000000000000000000000000000000..6456dab3cceef9c0b81a6a15d5f0df22fab547b6 --- /dev/null +++ b/FitConnect/Interfaces/ISubscriber.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using FitConnect.Models; + +namespace FitConnect; + +public interface ISubscriber : IBaseFunctionality { + /// <summary> + /// Decrypt Data (Fachdaten) + /// </summary> + /// <param name="data">(Fachdaten)</param> + /// <param name="privateKey">Your private key for decryption</param> + /// <returns></returns> + Task<string> DecryptDataAsync(string data, string privateKey); + + // Decrypt attachments (Anhänge) + Task<string> DecryptAttachmentAsync(string attachment, string privateKey); + + /// <summary> + /// Checks the validity of the given metadata against the schema. + /// </summary> + /// <param name="jsonMetaData">JSON meta data</param> + /// <returns></returns> + Task<bool> CheckMetadataAsync(string jsonMetaData); + + // Check Hash from Metadata + Task<bool> CheckHashAsync(string metadata); + + // Create SecurityEventToken and signature + Task<SecurityEventToken> CreateSecurityEventTokenAsync(string data, string attachment, + string privateKey); +} \ No newline at end of file diff --git a/FitConnect/Models/OAuthAccessToken.cs b/FitConnect/Models/OAuthAccessToken.cs new file mode 100644 index 0000000000000000000000000000000000000000..87ac3872e52a9a9e88e1df33eace09bb8fbd3ed1 --- /dev/null +++ b/FitConnect/Models/OAuthAccessToken.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; + +namespace FitConnect.Models; + +public class OAuthAccessToken { + [JsonPropertyName("access_token")] + public string AccessToken { get; set; } + + [JsonPropertyName("scope")] + public string Scope { get; set; } + + [JsonPropertyName("token_type")] + public string TokenType { get; set; } + + [JsonPropertyName("expires_in")] + public int ExpiresIn { get; set; } +} diff --git a/working_notes.md b/working_notes.md new file mode 100644 index 0000000000000000000000000000000000000000..517d433b53dbfa873b286a70452895f17e60a3eb --- /dev/null +++ b/working_notes.md @@ -0,0 +1,31 @@ +# Notes + +# TODOS + +| interface | implemented | tested | scope | description | +|:---------:|:-----------:|:------:|:-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------| +| X | X | X | Sender,Subscriber | Abruf von OAuth-Tokens | +| X | | | Sender | Prüfung von öffentlichen Schlüsseln und Zertifikatsketten + OCSP-Check (vgl. [#119](https://git.fitko.de/fit-connect/planning/-/issues/119)) | +| X | | | Sender | Verschlüsselung von Fachdaten (JSON, XML) mittels JWE | +| X | | | Sender | Verschlüsselung von Anhängen (Binärdaten) mittels JWE | +| X | | | Sender | Korrekte Erzeugung eines Metadatensatzes inkl. [Hashwerte](https://docs.fitko.de/fit-connect/docs/sending/metadata#integrity) | +| X | | | Subscriber | Entschlüsselung von Fachdaten (JSON oder XML) mittels JWE | +| X | | | Subscriber | Entschlüsselung von Anhängen (Binärdaten) mittels JWE | +| X | X | | Subscriber | Prüfung der empfangenen Metadaten gegen das zugehörige JSON-Schema | +| X | | | Subscriber | [Prüfung der Hashwerte](https://docs.fitko.de/fit-connect/docs/receiving/verification#integrity) aus dem Metadatensatz. | +| X | | | Subscriber | SET-Erstellung inkl. Signaturerzeugung | +| X | | | Sender, Subscriber | SET-Empfang inkl. Signaturprüfung | +| X | | | Sender, Subscriber | Unterstüzung / Abstraktion der API-Nutzung (`fitconnect.sendSubmission(metadata, destinationID, ...)` o.ä.) für die oben genannten Use-Cases | +| X | | | Sender, Subscriber | Logging (Logging-Modul muss von außen kommen) | + +## Links + +- [SDK-Konzept im Wiki](https://wiki.fit-connect.fitko.dev/de/Konzeption/Konzeption_SDK) +- [inoffizielles Python-SDK](https://github.com/codedust/fitconnect-sdk-python) +- [Project management](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Projektvorgehensmodell) +- [Wiki SDK Description](https://wiki.fit-connect.fitko.dev/de/PM_PUBLIC/Epics/SDK_Initialisierung) +- [Containing GitLab](https://git.fitko.de/) +- [Board filtered for SDK](https://git.fitko.de/fit-connect/planning/-/boards/44?label_name%5B%5D=component%3A%3ASDK) +- [Documentation](https://docs.fitko.de/fit-connect/docs/getting-started/first-steps/) +- [Security Event Token Requirements](https://wiki.fit-connect.fitko.dev/de/Konzeption/Security_Event_Token_Future) +- [glossary](https://docs.fitko.de/fit-connect/docs/glossary/)