commit 121052a3ef9800a5500a85ffe480a29e95b5b21a Author: Alex Bezdieniezhnykh Date: Sat Nov 9 00:37:43 2024 +0200 Init commit add security encryption and hashing: WIP add endpoints: register user, get and save resources add db main operations, User entity diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b9cbb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +bin +obj +.vs +*.DotSettings* +*.user +log* \ No newline at end of file diff --git a/Azaion.Api.sln b/Azaion.Api.sln new file mode 100644 index 0000000..f8d089b --- /dev/null +++ b/Azaion.Api.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Api", "Azaion.Api\Azaion.Api.csproj", "{03A56CF2-A57F-4631-8454-C08B804B8903}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Common\Azaion.Common.csproj", "{E838FA94-B96D-4446-B5D6-6BC1A34436C1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Services", "Azaion.Services\Azaion.Services.csproj", "{07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Test", "Azaion.Test\Azaion.Test.csproj", "{2F4F0EA9-0645-4917-8D21-F317E815EB9E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {03A56CF2-A57F-4631-8454-C08B804B8903}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03A56CF2-A57F-4631-8454-C08B804B8903}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03A56CF2-A57F-4631-8454-C08B804B8903}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03A56CF2-A57F-4631-8454-C08B804B8903}.Release|Any CPU.Build.0 = Release|Any CPU + {E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E838FA94-B96D-4446-B5D6-6BC1A34436C1}.Release|Any CPU.Build.0 = Release|Any CPU + {07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {07CFFA74-A1ED-43F9-9CD4-5A09B320EF44}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4F0EA9-0645-4917-8D21-F317E815EB9E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Azaion.Api/Azaion.Api.csproj b/Azaion.Api/Azaion.Api.csproj new file mode 100644 index 0000000..963b42e --- /dev/null +++ b/Azaion.Api/Azaion.Api.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/Azaion.Api/Azaion.Api.http b/Azaion.Api/Azaion.Api.http new file mode 100644 index 0000000..08f7a1f --- /dev/null +++ b/Azaion.Api/Azaion.Api.http @@ -0,0 +1,6 @@ +@Azaion.Api_HostAddress = http://localhost:5219 + +GET {{Azaion.Api_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/Azaion.Api/Program.cs b/Azaion.Api/Program.cs new file mode 100644 index 0000000..c9dd83f --- /dev/null +++ b/Azaion.Api/Program.cs @@ -0,0 +1,48 @@ +using Azaion.Common.Configs; +using Azaion.Common.Database; +using Azaion.Common.Requests; +using Azaion.Services; +using FluentValidation; +using Microsoft.Extensions.Options; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.Configure(builder.Configuration.GetSection(nameof(ResourcesConfig))); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddSingleton(sp => new DbFactory(sp.GetService>()!.Value.AzaionDb)); + +builder.Services.AddValidatorsFromAssemblyContaining(); + + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.MapPost("/register-user", + async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken) + => await userService.RegisterUser(registerUserRequest, cancellationToken)); + +app.MapPost("/resources/get", + async (GetResourceRequest getResourceRequest, IUserService userService, IResourcesService resourcesService, CancellationToken cancellationToken) => + { + await userService.ValidateUser(getResourceRequest, cancellationToken); + var ms = new MemoryStream(); + await resourcesService.GetEncryptedResource(getResourceRequest, ms, cancellationToken); + return ms; + }); + +app.MapPost("/resources", + async (UploadResourceRequest uploadResourceRequest, IResourcesService resourceService, CancellationToken cancellationToken) + => await resourceService.SaveResource(uploadResourceRequest, cancellationToken)); + +app.Run(); \ No newline at end of file diff --git a/Azaion.Api/Properties/launchSettings.json b/Azaion.Api/Properties/launchSettings.json new file mode 100644 index 0000000..7532d32 --- /dev/null +++ b/Azaion.Api/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:10133", + "sslPort": 44330 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5219", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7139;http://localhost:5219", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Azaion.Api/appsettings.Development.json b/Azaion.Api/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/Azaion.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Azaion.Api/appsettings.json b/Azaion.Api/appsettings.json new file mode 100644 index 0000000..4843f7d --- /dev/null +++ b/Azaion.Api/appsettings.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ResourcesConfig": { + "ResourcesFolder": "Content", + "Resources": { + "AnnotatorDll": "Azaion.Annotator.dll", + "AIModelONNX": "azaion.onnx", + "AIModelRKNN": "azaion.rknn" + } + } +} diff --git a/Azaion.Common/Azaion.Common.csproj b/Azaion.Common/Azaion.Common.csproj new file mode 100644 index 0000000..1b8e386 --- /dev/null +++ b/Azaion.Common/Azaion.Common.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Azaion.Common/BusinessException.cs b/Azaion.Common/BusinessException.cs new file mode 100644 index 0000000..9146cc5 --- /dev/null +++ b/Azaion.Common/BusinessException.cs @@ -0,0 +1,18 @@ +namespace Azaion.Common; + +public class BusinessException(ExceptionEnum exEnum, string message) : Exception(message) +{ + private ExceptionEnum ExceptionEnum { get; set; } = exEnum; +} + +public enum ExceptionEnum +{ + NoUserFound = 10, + UserExists = 20, + PasswordIncorrect = 30, + UserLengthIncorrect = 33, + WrongEmail = 35, + PasswordLengthIncorrect = 37, + HardwareIdMismatch = 40, + WrongResourceType = 50 +} \ No newline at end of file diff --git a/Azaion.Common/Configs/ConnectionStrings.cs b/Azaion.Common/Configs/ConnectionStrings.cs new file mode 100644 index 0000000..7db8c2f --- /dev/null +++ b/Azaion.Common/Configs/ConnectionStrings.cs @@ -0,0 +1,6 @@ +namespace Azaion.Common.Configs; + +public class ConnectionStrings +{ + public string AzaionDb { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Configs/ResourcesConfig.cs b/Azaion.Common/Configs/ResourcesConfig.cs new file mode 100644 index 0000000..6fe64db --- /dev/null +++ b/Azaion.Common/Configs/ResourcesConfig.cs @@ -0,0 +1,7 @@ +namespace Azaion.Common.Configs; + +public class ResourcesConfig +{ + public string ResourcesFolder { get; set; } = null!; + public Dictionary Resources { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Database/AzaionDb.cs b/Azaion.Common/Database/AzaionDb.cs new file mode 100644 index 0000000..0ff794a --- /dev/null +++ b/Azaion.Common/Database/AzaionDb.cs @@ -0,0 +1,10 @@ +using Azaion.Common.Entities; +using LinqToDB; +using LinqToDB.Data; + +namespace Azaion.Common.Database; + +public class AzaionDb(DataOptions dataOptions) : DataConnection(dataOptions) +{ + public ITable Users => this.GetTable(); +} \ No newline at end of file diff --git a/Azaion.Common/Database/AzaionDbShemaHolder.cs b/Azaion.Common/Database/AzaionDbShemaHolder.cs new file mode 100644 index 0000000..4b03c44 --- /dev/null +++ b/Azaion.Common/Database/AzaionDbShemaHolder.cs @@ -0,0 +1,21 @@ +using Azaion.Common.Entities; +using LinqToDB.Mapping; + +namespace Azaion.Common.Database; + +public static class AzaionDbSchemaHolder +{ + public static readonly MappingSchema MappingSchema; + + static AzaionDbSchemaHolder() + { + MappingSchema = new MappingSchema(); + var builder = new FluentMappingBuilder(MappingSchema); + + builder.Entity() + .HasTableName("users") + .HasIdentity(x => x.Id); + + builder.Build(); + } +} \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs new file mode 100644 index 0000000..600e298 --- /dev/null +++ b/Azaion.Common/Database/DbFactory.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; +using LinqToDB; + +namespace Azaion.Common.Database; + +public interface IDbFactory +{ + Task Run(Func> func); + Task Run(Func func); + + T Run(Func func); +} + +public class DbFactory : IDbFactory +{ + private readonly DataOptions _dataOptions; + + public DbFactory(string connectionString, bool useTracing = true, bool msSql = false) + { + if (string.IsNullOrEmpty(connectionString)) + throw new ArgumentException("Empty connectionString", nameof(connectionString)); + + _dataOptions = new DataOptions() + .UsePostgreSQL(connectionString) + .UseMappingSchema(AzaionDbSchemaHolder.MappingSchema); + + if (useTracing) + _ = _dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText)); + } + + public async Task Run(Func> func) + { + await using var db = new AzaionDb(_dataOptions); + return await func(db); + } + + public async Task Run(Func func) + { + await using var db = new AzaionDb(_dataOptions); + await func(db); + } + + public T Run(Func func) + { + using var db = new AzaionDb(_dataOptions); + return func(db); + } +} diff --git a/Azaion.Common/Entities/ResourceEnum.cs b/Azaion.Common/Entities/ResourceEnum.cs new file mode 100644 index 0000000..858667c --- /dev/null +++ b/Azaion.Common/Entities/ResourceEnum.cs @@ -0,0 +1,8 @@ +namespace Azaion.Common.Entities; + +public enum ResourceEnum +{ + AnnotatorDll = 10, + AIModelRKNN = 20, + AIModelONNX = 20, +} \ No newline at end of file diff --git a/Azaion.Common/Entities/RoleEnum.cs b/Azaion.Common/Entities/RoleEnum.cs new file mode 100644 index 0000000..07bb839 --- /dev/null +++ b/Azaion.Common/Entities/RoleEnum.cs @@ -0,0 +1,9 @@ +namespace Azaion.Common; + +public enum RoleEnum +{ + Operator, + Validator, + CompanionPC, + Admin +} diff --git a/Azaion.Common/Entities/User.cs b/Azaion.Common/Entities/User.cs new file mode 100644 index 0000000..335e799 --- /dev/null +++ b/Azaion.Common/Entities/User.cs @@ -0,0 +1,12 @@ +namespace Azaion.Common.Entities; + +public class User +{ + public string Id { get; set; } = null!; + public string Username { get; set; } = null!; + public string PasswordHash { get; set; } = null!; + public string HardwareId { get; set; } = null!; + public RoleEnum Role { get; set; } + + public string UniqueKey => $"Azaion#{Username}#{PasswordHash}#{HardwareId}"; +} \ No newline at end of file diff --git a/Azaion.Common/Requests/GetResourceRequest.cs b/Azaion.Common/Requests/GetResourceRequest.cs new file mode 100644 index 0000000..06e3052 --- /dev/null +++ b/Azaion.Common/Requests/GetResourceRequest.cs @@ -0,0 +1,17 @@ +using Azaion.Common.Entities; + +namespace Azaion.Common.Requests; + +public class GetResourceRequest +{ + public string Username { get; set; } = null!; + public string Password { get; set; } = null!; + public string HardwareId { get; set; } = null!; + public ResourceEnum ResourceEnum { get; set; } +} + +public class UploadResourceRequest +{ + public ResourceEnum ResourceEnum { get; set; } + public Stream Data { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Requests/RegisterUserRequest.cs b/Azaion.Common/Requests/RegisterUserRequest.cs new file mode 100644 index 0000000..ad0508c --- /dev/null +++ b/Azaion.Common/Requests/RegisterUserRequest.cs @@ -0,0 +1,22 @@ +using FluentValidation; + +namespace Azaion.Common.Requests; + +public class RegisterUserRequest +{ + public string Email { get; set; } = null!; + public string Password { get; set; } = null!; + public RoleEnum Role { get; set; } +} + +public class RegisterUserValidator : AbstractValidator +{ + public RegisterUserValidator() + { + RuleFor(r => r.Email) + .MinimumLength(8).WithErrorCode(ExceptionEnum.UserLengthIncorrect.ToString()).WithMessage("Email address should be at least 8 characters.") + .EmailAddress().WithErrorCode(ExceptionEnum.WrongEmail.ToString()).WithMessage("Email address is not valid."); + + RuleFor(r => r.Password) + .MinimumLength(8).WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()).WithMessage("Password should be at least 8 characters."); + } } \ No newline at end of file diff --git a/Azaion.Services/Azaion.Services.csproj b/Azaion.Services/Azaion.Services.csproj new file mode 100644 index 0000000..7feaa28 --- /dev/null +++ b/Azaion.Services/Azaion.Services.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll + + + + diff --git a/Azaion.Services/ResourcesService.cs b/Azaion.Services/ResourcesService.cs new file mode 100644 index 0000000..4d3d4f1 --- /dev/null +++ b/Azaion.Services/ResourcesService.cs @@ -0,0 +1,37 @@ +using Azaion.Common; +using Azaion.Common.Configs; +using Azaion.Common.Entities; +using Azaion.Common.Requests; +using Microsoft.Extensions.Options; + +namespace Azaion.Services; + +public interface IResourcesService +{ + Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default); + Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default); +} + +public class ResourcesService(IOptions resourcesConfig) : IResourcesService +{ + public async Task GetEncryptedResource(GetResourceRequest request, Stream outputStream, CancellationToken cancellationToken = default) + { + var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.Open, FileAccess.Read); + var key = Security.MakeEncryptionKey(request.Username, request.Password); + await fileStream.Encrypt(outputStream, key, cancellationToken); + } + + public async Task SaveResource(UploadResourceRequest request, CancellationToken cancellationToken = default) + { + await using var fileStream = new FileStream(GetResourcePath(request.ResourceEnum), FileMode.OpenOrCreate, FileAccess.ReadWrite); + await request.Data.CopyToAsync(fileStream, cancellationToken); + } + + private string GetResourcePath(ResourceEnum resourceEnum) + { + var resource = resourcesConfig.Value.Resources.GetValueOrDefault(resourceEnum.ToString()); + if (resource == null) + throw new BusinessException(ExceptionEnum.WrongResourceType, "Wrong resource type!"); + return Path.Combine(resourcesConfig.Value.ResourcesFolder, resource); + } +} \ No newline at end of file diff --git a/Azaion.Services/Security.cs b/Azaion.Services/Security.cs new file mode 100644 index 0000000..0040a28 --- /dev/null +++ b/Azaion.Services/Security.cs @@ -0,0 +1,56 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Azaion.Services; + +public static class Security +{ + private const int BUFFER_SIZE = 81920; // 80 KB buffer size + + public static string ToHash(this string str) => + Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); + + public static string MakeEncryptionKey(string username, string password) => + $"{username}-{password}---#%@AzaionKey@%#---"; + + public static async Task Encrypt(this Stream stream, Stream outputStream, string key, CancellationToken cancellationToken = default) + { + if (stream is { CanSeek: false }) throw new ArgumentNullException(nameof(stream)); + if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key)); + + using var aes = Aes.Create(); + aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); + aes.GenerateIV(); + + using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); + await using var cs = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write); + + // Prepend IV to the encrypted data + await outputStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken); + + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0) + await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + } + + public static async Task Decrypt(this Stream encryptedStream, Stream outputStream, string key, CancellationToken cancellationToken = default) + { + using var aes = Aes.Create(); + aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key)); + + // Read the IV from the start of the input stream + var iv = new byte[aes.BlockSize / 8]; + _ = await encryptedStream.ReadAsync(iv, cancellationToken); + aes.IV = iv; + + using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV); + await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read); + + // Read and write in chunks + var buffer = new byte[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0) + await outputStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken); + } +} diff --git a/Azaion.Services/UserService.cs b/Azaion.Services/UserService.cs new file mode 100644 index 0000000..3038eae --- /dev/null +++ b/Azaion.Services/UserService.cs @@ -0,0 +1,56 @@ +using Azaion.Common; +using Azaion.Common.Database; +using Azaion.Common.Entities; +using Azaion.Common.Requests; +using LinqToDB; + +namespace Azaion.Services; + +public interface IUserService +{ + Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); + Task ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default); +} + +public class UserService(IDbFactory dbFactory) : IUserService +{ + public async Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default) + { + await dbFactory.Run(async db => + { + var existingUser = await db.Users.FirstOrDefaultAsync(u => u.Username == request.Email, token: cancellationToken); + if (existingUser != null) + throw new BusinessException(ExceptionEnum.UserExists, "User already exists"); + + await db.InsertAsync(new User + { + Username = request.Email, + PasswordHash = request.Password.ToHash(), + Role = request.Role + }, token: cancellationToken); + }); + } + + public async Task ValidateUser(GetResourceRequest request, CancellationToken cancellationToken = default) => + await dbFactory.Run(async db => + { + var user = await db.Users.FirstOrDefaultAsync(x => x.Username == request.Username, token: cancellationToken); + if (user == null) + throw new BusinessException(ExceptionEnum.NoUserFound, "No user found"); + + if (request.Password.ToHash() != user.PasswordHash) + throw new BusinessException(ExceptionEnum.PasswordIncorrect, "Passwords do not match"); + + //If user's hardware Id is empty (usually on the first time login), then write down user + if (string.IsNullOrEmpty(user.HardwareId)) + await db.Users.UpdateAsync(x => x.Username == request.Username, u => new User{HardwareId = request.HardwareId}, token: cancellationToken); + else + { + //But if hardware Id exists, it should match with request + if (user.HardwareId != request.HardwareId) + throw new BusinessException(ExceptionEnum.HardwareIdMismatch, "Hardware id mismatch"); + } + + return user; + }); +} diff --git a/Azaion.Test/Azaion.Test.csproj b/Azaion.Test/Azaion.Test.csproj new file mode 100644 index 0000000..2d78392 --- /dev/null +++ b/Azaion.Test/Azaion.Test.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Azaion.Test/SecurityTest.cs b/Azaion.Test/SecurityTest.cs new file mode 100644 index 0000000..2fb943b --- /dev/null +++ b/Azaion.Test/SecurityTest.cs @@ -0,0 +1,41 @@ +using System.Text; +using Azaion.Services; +using FluentAssertions; +using Xunit; + +namespace Azaion.Test; + +public class SecurityTest +{ + [Fact] + public async Task EncryptDecryptTest() + { + var testString = "Hello World Test dfvjkhsdbfvkljh sabdljsdafv asdv"; + var username = "user@azaion.com"; + var password = "testpw"; + var key = Security.MakeEncryptionKey(username, password); + + await using var encryptedStream = new MemoryStream(); + await StringToStream(testString).Encrypt(encryptedStream, key); + + await using var decryptedStream = new MemoryStream(); + await encryptedStream.Decrypt(decryptedStream, key); + + var str = StreamToString(decryptedStream); + str.Should().Be(testString); + } + + private static string StreamToString(Stream stream) + { + stream.Position = 0; + using var reader = new StreamReader(stream, Encoding.UTF8); + return reader.ReadToEnd(); + } + + private static Stream StringToStream(string src) + { + var byteArray = Encoding.UTF8.GetBytes(src); + return new MemoryStream(byteArray); + } + +} \ No newline at end of file