Init commit

add security encryption and hashing: WIP
add endpoints: register user, get and save resources
add db main operations, User entity
This commit is contained in:
Alex Bezdieniezhnykh
2024-11-09 00:37:43 +02:00
commit 121052a3ef
26 changed files with 605 additions and 0 deletions
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Options">
<HintPath>C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App\8.0.8\Microsoft.Extensions.Options.dll</HintPath>
</Reference>
</ItemGroup>
</Project>
+37
View File
@@ -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> 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);
}
}
+56
View File
@@ -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);
}
}
+56
View File
@@ -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<User?> 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<User?> 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;
});
}