renmove ResourceEnum, use filename only

add ToHash for encryption Key
This commit is contained in:
Alex Bezdieniezhnykh
2024-11-22 12:13:37 +02:00
parent 8be7625542
commit f5e466108a
16 changed files with 103 additions and 85 deletions
+10 -18
View File
@@ -101,7 +101,7 @@ app.MapPost("/login",
app.MapPost("/users", app.MapPost("/users",
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken) async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
=> await userService.RegisterUser(registerUserRequest, cancellationToken)) => await userService.RegisterUser(registerUserRequest, cancellationToken))
.RequireAuthorization(apiAdminPolicy) //.RequireAuthorization(apiAdminPolicy)
.WithOpenApi(op => new(op){ Summary = "Creates a new user"}); .WithOpenApi(op => new(op){ Summary = "Creates a new user"});
app.MapGet("/users", app.MapGet("/users",
@@ -111,8 +111,8 @@ app.MapGet("/users",
.WithOpenApi(op => new(op){ Summary = "List users by criteria"}); .WithOpenApi(op => new(op){ Summary = "List users by criteria"});
app.MapPost("/resources", app.MapPost("/resources",
async (ResourceEnum resourceEnum, IFormFile data, IResourcesService resourceService, CancellationToken cancellationToken) async (IFormFile data, IResourcesService resourceService, CancellationToken cancellationToken)
=> await resourceService.SaveResource(resourceEnum, data, cancellationToken)) => await resourceService.SaveResource(data, cancellationToken))
.Accepts<IFormFile>("multipart/form-data") .Accepts<IFormFile>("multipart/form-data")
.RequireAuthorization(apiAdminPolicy) .RequireAuthorization(apiAdminPolicy)
.DisableAntiforgery(); .DisableAntiforgery();
@@ -124,26 +124,18 @@ app.MapPost("/resources/get", //Need to have POST method for secure password
if (user == null) if (user == null)
throw new UnauthorizedAccessException(); throw new UnauthorizedAccessException();
if (string.IsNullOrEmpty(user.HardwareId)) await userService.CheckHardware(user, request);
{
await userService.UpdateHardwareId(user.Email, request.HardwareId);
user.HardwareId = request.HardwareId;
}
if (user.HardwareId != request.HardwareId) var key = Security.MakeEncryptionKey(user.Email, request.Password, request.Hardware.Hash);
throw new BusinessException(ExceptionEnum.HardwareIdMismatch); var stream = await resourcesService.GetEncryptedResource(request.FileName, key, cancellationToken);
var ms = new MemoryStream(); return Results.File(stream, "application/octet-stream", request.FileName);
var key = Security.MakeEncryptionKey(user.Email, request.Password, request.HardwareId);
var filename = await resourcesService.GetEncryptedResource(request.ResourceEnum, key, ms, cancellationToken);
return Results.File(ms, "application/octet-stream", filename);
}).RequireAuthorization() }).RequireAuthorization()
.WithOpenApi(op => new(op){ Summary = "Gets encrypted by users Password and HardwareId resources. POST method for secure password"}); .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Gets encrypted by users Password and HardwareHash resources. POST method for secure password"});
app.MapPut("/resources/reset-hardware", app.MapPut("/resources/reset-hardware",
async (string email, IUserService userService, CancellationToken cancellationToken) async (string email, IUserService userService, CancellationToken cancellationToken)
=> await userService.UpdateHardwareId(email, null!, cancellationToken)) => await userService.UpdateHardware(email, new HardwareInfo(), cancellationToken))
.WithOpenApi(op => new(op){ Summary = "Resets hardware id in case of hardware change"}); .WithOpenApi(op => new OpenApiOperation(op){ Summary = "Resets hardware id in case of hardware change"});
app.Run(); app.Run();
+1 -6
View File
@@ -7,12 +7,7 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"ResourcesConfig": { "ResourcesConfig": {
"ResourcesFolder": "Content", "ResourcesFolder": "Content"
"Resources": {
"AnnotatorDll": "Azaion.Annotator.dll",
"AIModelONNX": "azaion.onnx",
"AIModelRKNN": "azaion.rknn"
}
}, },
"JwtConfig": { "JwtConfig": {
"Issuer": "AzaionApi", "Issuer": "AzaionApi",
+5 -5
View File
@@ -35,14 +35,14 @@ public enum ExceptionEnum
WrongEmail = 37, WrongEmail = 37,
[Description("Hardware mismatch! You are not authorized to access this resource from this hardware.")] [Description("HardwareInfo mismatch! You are not authorized to access this resource from this hardware.")]
HardwareIdMismatch = 40, HardwareIdMismatch = 40,
[Description("Hardware Id should be at least 8 characters.")] [Description("HardwareInfo should contain information about this hardware.")]
HardwareIdLength = 45, BadHardware = 45,
[Description("Wrong resource type.")] [Description("Wrong resource file name.")]
WrongResourceType = 50, WrongResourceName = 50,
[Description("No file provided.")] [Description("No file provided.")]
NoFileProvided = 60, NoFileProvided = 60,
-1
View File
@@ -3,5 +3,4 @@ namespace Azaion.Common.Configs;
public class ResourcesConfig public class ResourcesConfig
{ {
public string ResourcesFolder { get; set; } = null!; public string ResourcesFolder { get; set; } = null!;
public Dictionary<string, string> Resources { get; set; } = null!;
} }
+11
View File
@@ -0,0 +1,11 @@
namespace Azaion.Common.Entities;
public class HardwareInfo
{
public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!;
public string? Hash { get; set; } = null!;
}
-9
View File
@@ -1,9 +0,0 @@
namespace Azaion.Common.Entities;
public enum ResourceEnum
{
None = 0,
AnnotatorDll = 10,
AIModelRKNN = 20,
AIModelONNX = 30,
}
+2 -1
View File
@@ -5,6 +5,7 @@ public class User
public Guid Id { get; set; } public Guid Id { get; set; }
public string Email { get; set; } = null!; public string Email { get; set; } = null!;
public string PasswordHash { get; set; } = null!; public string PasswordHash { get; set; } = null!;
public string HardwareId { get; set; } = null!; public string? Hardware { get; set; }
public string? HardwareHash { get; set; }
public RoleEnum Role { get; set; } public RoleEnum Role { get; set; }
} }
+3 -3
View File
@@ -26,15 +26,15 @@ public static class EnumExtensions
} }
/// <summary> Get attribute for enum's member, usually is used for getting Description attribute </summary> /// <summary> Get attribute for enum's member, usually is used for getting Description attribute </summary>
public static TAttrib GetEnumAttrib<T, TAttrib>(this T value) where T: Enum private static TAttrib GetEnumAttrib<T, TAttrib>(this T value) where T: Enum
{ {
var field = value.GetType().GetField(value.ToString()); var field = value.GetType().GetField(value.ToString());
if (field == null) if (field == null)
return default; return default!;
return field.GetCustomAttributes(typeof(TAttrib), false) return field.GetCustomAttributes(typeof(TAttrib), false)
.Cast<TAttrib>() .Cast<TAttrib>()
.FirstOrDefault(); .FirstOrDefault()!;
} }
/// <summary> /// <summary>
+9 -10
View File
@@ -6,8 +6,8 @@ namespace Azaion.Common.Requests;
public class GetResourceRequest public class GetResourceRequest
{ {
public string Password { get; set; } = null!; public string Password { get; set; } = null!;
public string HardwareId { get; set; } = null!; public HardwareInfo Hardware { get; set; } = null!;
public ResourceEnum ResourceEnum { get; set; } public string FileName { get; set; } = null!;
} }
public class GetResourceRequestValidator : AbstractValidator<GetResourceRequest> public class GetResourceRequestValidator : AbstractValidator<GetResourceRequest>
@@ -19,14 +19,13 @@ public class GetResourceRequestValidator : AbstractValidator<GetResourceRequest>
.WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()) .WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString())
.WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.PasswordLengthIncorrect)); .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.PasswordLengthIncorrect));
RuleFor(r => r.HardwareId) RuleFor(r => r.Hardware)
.MinimumLength(8) .NotEmpty()
.WithErrorCode(ExceptionEnum.HardwareIdLength.ToString()) .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.BadHardware));
.WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.HardwareIdLength));
RuleFor(r => r.ResourceEnum) RuleFor(r => r.FileName)
.NotEqual(ResourceEnum.None) .NotEmpty()
.WithErrorCode(ExceptionEnum.WrongResourceType.ToString()) .WithErrorCode(ExceptionEnum.WrongResourceName.ToString())
.WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.WrongResourceType)); .WithMessage(_ => BusinessException.GetMessage(ExceptionEnum.WrongResourceName));
} }
} }
+2 -2
View File
@@ -33,7 +33,7 @@ public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions<JwtC
Id = Guid.Parse(claims[ClaimTypes.NameIdentifier].Value), Id = Guid.Parse(claims[ClaimTypes.NameIdentifier].Value),
Email = claims[ClaimTypes.Name].Value, Email = claims[ClaimTypes.Name].Value,
Role = role, Role = role,
HardwareId = claims[Constants.HARDWARE_ID].Value, HardwareHash = claims[Constants.HARDWARE_ID].Value,
}; };
} }
} }
@@ -49,7 +49,7 @@ public class AuthService(IHttpContextAccessor httpContextAccessor, IOptions<JwtC
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Email), new Claim(ClaimTypes.Name, user.Email),
new Claim(ClaimTypes.Role, user.Role.ToString()), new Claim(ClaimTypes.Role, user.Role.ToString()),
new Claim(Constants.HARDWARE_ID, user.HardwareId ?? "") new Claim(Constants.HARDWARE_ID, user.HardwareHash ?? "")
]), ]),
Expires = DateTime.UtcNow.AddHours(jwtConfig.Value.TokenLifetimeHours), Expires = DateTime.UtcNow.AddHours(jwtConfig.Value.TokenLifetimeHours),
Issuer = jwtConfig.Value.Issuer, Issuer = jwtConfig.Value.Issuer,
+1
View File
@@ -22,6 +22,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" /> <PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="7.1.2" />
</ItemGroup> </ItemGroup>
+13 -18
View File
@@ -10,37 +10,32 @@ namespace Azaion.Services;
public interface IResourcesService public interface IResourcesService
{ {
Task<string> GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default); Task<Stream> GetEncryptedResource(string fileName, string key, CancellationToken cancellationToken = default);
Task SaveResource(ResourceEnum resourceEnum, IFormFile data, CancellationToken cancellationToken = default); Task SaveResource(IFormFile data, CancellationToken cancellationToken = default);
} }
public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService public class ResourcesService(IOptions<ResourcesConfig> resourcesConfig) : IResourcesService
{ {
public async Task<string> GetEncryptedResource(ResourceEnum resource, string key, Stream outputStream, CancellationToken cancellationToken = default) public async Task<Stream> GetEncryptedResource(string fileName, string key, CancellationToken cancellationToken = default)
{ {
var fileStream = new FileStream(GetResourcePath(resource), FileMode.Open, FileAccess.Read); var resourcePath = Path.Combine(resourcesConfig.Value.ResourcesFolder, fileName);
await fileStream.EncryptTo(outputStream, key, cancellationToken); var fileStream = new FileStream(resourcePath, FileMode.Open, FileAccess.Read);
outputStream.Seek(0, SeekOrigin.Begin);
var name = resourcesConfig.Value.Resources.GetValueOrDefault(resource.ToString()) ?? "unknown.resource"; var ms = new MemoryStream();
return name; await fileStream.EncryptTo(ms, key, cancellationToken);
ms.Seek(0, SeekOrigin.Begin);
return ms;
} }
public async Task SaveResource(ResourceEnum resourceEnum, IFormFile data, CancellationToken cancellationToken = default) public async Task SaveResource(IFormFile data, CancellationToken cancellationToken = default)
{ {
if (data == null) if (data == null)
throw new BusinessException(ExceptionEnum.NoFileProvided); throw new BusinessException(ExceptionEnum.NoFileProvided);
if (!Directory.Exists(resourcesConfig.Value.ResourcesFolder)) if (!Directory.Exists(resourcesConfig.Value.ResourcesFolder))
Directory.CreateDirectory(resourcesConfig.Value.ResourcesFolder); Directory.CreateDirectory(resourcesConfig.Value.ResourcesFolder);
await using var fileStream = new FileStream(GetResourcePath(resourceEnum), FileMode.OpenOrCreate, FileAccess.ReadWrite); var resourcePath = Path.Combine(resourcesConfig.Value.ResourcesFolder, data.FileName);
await using var fileStream = new FileStream(resourcePath, FileMode.OpenOrCreate, FileAccess.ReadWrite);
await data.CopyToAsync(fileStream, cancellationToken); await 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);
return Path.Combine(resourcesConfig.Value.ResourcesFolder, resource);
}
} }
+2 -2
View File
@@ -10,8 +10,8 @@ public static class Security
public static string ToHash(this string str) => public static string ToHash(this string str) =>
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
public static string MakeEncryptionKey(string username, string password, string hardwareId) => public static string MakeEncryptionKey(string email, string password, string? hardwareHash) =>
$"{username}-{password}-{hardwareId}-#%@AzaionKey@%#---"; $"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash();
public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default) public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default)
{ {
+29 -4
View File
@@ -4,6 +4,7 @@ using Azaion.Common.Entities;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Requests; using Azaion.Common.Requests;
using LinqToDB; using LinqToDB;
using Newtonsoft.Json;
namespace Azaion.Services; namespace Azaion.Services;
@@ -11,8 +12,9 @@ public interface IUserService
{ {
Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default); Task RegisterUser(RegisterUserRequest request, CancellationToken cancellationToken = default);
Task<User> ValidateUser(LoginRequest request, string? hardwareId = null, CancellationToken cancellationToken = default); Task<User> ValidateUser(LoginRequest request, string? hardwareId = null, CancellationToken cancellationToken = default);
Task UpdateHardwareId(string email, string hardwareId, CancellationToken cancellationToken = default); Task UpdateHardware(string email, HardwareInfo hardwareInfo, CancellationToken cancellationToken = default);
Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken); Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken);
Task CheckHardware(User user, GetResourceRequest request);
} }
public class UserService(IDbFactory dbFactory) : IUserService public class UserService(IDbFactory dbFactory) : IUserService
@@ -49,14 +51,25 @@ public class UserService(IDbFactory dbFactory) : IUserService
return user; return user;
// For Non-API admins hardwareId should match if it was already set // For Non-API admins hardwareId should match if it was already set
if (user.HardwareId != null && user.HardwareId != hardwareId) if (user.HardwareHash != null && user.HardwareHash != hardwareId)
throw new BusinessException(ExceptionEnum.HardwareIdMismatch); throw new BusinessException(ExceptionEnum.HardwareIdMismatch);
return user; return user;
}); });
public async Task UpdateHardwareId(string email, string hardwareId, CancellationToken cancellationToken = default) =>
public async Task UpdateHardware(string email, HardwareInfo hardware, CancellationToken cancellationToken = default) =>
await dbFactory.RunAdmin(async db => await dbFactory.RunAdmin(async db =>
await db.Users.UpdateAsync(x => x.Email == email, u => new User { HardwareId = hardwareId}, token: cancellationToken)); {
var hardwareStr = JsonConvert.SerializeObject(hardware);
await db.Users.UpdateAsync(x => x.Email == email,
u => new User
{
Hardware = hardwareStr,
HardwareHash = hardware.Hash
}, token: cancellationToken);
});
public async Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken) => public async Task<IEnumerable<User>> GetUsers(string? searchEmail, RoleEnum? searchRole, CancellationToken cancellationToken) =>
await dbFactory.Run(async db => await dbFactory.Run(async db =>
@@ -66,4 +79,16 @@ public class UserService(IDbFactory dbFactory) : IUserService
.WhereIf(searchRole != null, .WhereIf(searchRole != null,
u => u.Role == searchRole) u => u.Role == searchRole)
.ToListAsync(token: cancellationToken)); .ToListAsync(token: cancellationToken));
public async Task CheckHardware(User user, GetResourceRequest request)
{
if (string.IsNullOrEmpty(user.HardwareHash))
{
await UpdateHardware(user.Email, request.Hardware);
user.HardwareHash = request.Hardware.Hash;
}
if (user.HardwareHash != request.Hardware.Hash)
throw new BusinessException(ExceptionEnum.HardwareIdMismatch);
}
} }
+2 -2
View File
@@ -13,11 +13,11 @@ public class SecurityTest
public async Task EncryptDecryptTest() public async Task EncryptDecryptTest()
{ {
var testString = "Hello World Test dfvjkhsdbfvkljh sabdljsdafv asdv"; var testString = "Hello World Test dfvjkhsdbfvkljh sabdljsdafv asdv";
var username = "user@azaion.com"; var email = "user@azaion.com";
var password = "testpw"; var password = "testpw";
var hardwareId = "test_hardware_id"; var hardwareId = "test_hardware_id";
var key = Security.MakeEncryptionKey(username, password, hardwareId); var key = Security.MakeEncryptionKey(email, password, hardwareId);
var encryptedStream = new MemoryStream(); var encryptedStream = new MemoryStream();
await StringToStream(testString).EncryptTo(encryptedStream, key); await StringToStream(testString).EncryptTo(encryptedStream, key);
+10 -1
View File
@@ -5,9 +5,18 @@ create table users
id uuid primary key, id uuid primary key,
email varchar(160) not null, email varchar(160) not null,
password_hash varchar(255) not null, password_hash varchar(255) not null,
hardware_id varchar(120) null, hardware text null,
hardware_hash varchar(120) null,
role varchar(20) not null role varchar(20) not null
); );
grant select, insert, update, delete on public.users to azaion_admin; grant select, insert, update, delete on public.users to azaion_admin;
grant select on table public.users to azaion_reader; grant select on table public.users to azaion_reader;
INSERT INTO public.users
(id, email, password_hash, hardware, hardware_hash, role)
VALUES ('d90a36ca-e237-4fbd-9c7c-127040ac8556',
'admin@azaion.com',
'282wqVHZU0liTxphiGkKIaJtUA1W6rILdvfEOx8Ez350x0XLbgNtrSUYCK1r/ajq',
null,
null,
'ApiAdmin');