From 6b5cb77f58cc641b3e945509164522a048b97219 Mon Sep 17 00:00:00 2001 From: Kuro <96239346+Kuarma@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:18:21 +0200 Subject: [PATCH 1/5] Currently rewriting Endpoints. Jwt and Ratelimiter working --- CortexRemote.csproj | 3 +- .../RemoteEndpointExtension.cs | 39 --------- Program.cs | 80 ++++++++++--------- .../RemoteEndpointExtension.cs | 54 +++++++++++++ .../Security/GetWindowsTokenParameters.cs | 4 +- .../Security/RefreshTokenManager.cs | 61 -------------- WindowsRemoteControl/Security/TokenValues.cs | 3 + .../Security/ValidationRecord.cs | 3 + .../Security/WindowsAuthController.cs | 47 +++++++++++ .../Security/WindowsDataProtection.cs | 18 +++-- .../WindowsExecutionCommands.cs | 47 ----------- 11 files changed, 164 insertions(+), 195 deletions(-) delete mode 100644 Extensions/ApiEndpointExtensionMethod/RemoteEndpointExtension.cs create mode 100644 WindowsRemoteControl/RemoteEndpointExtension.cs delete mode 100644 WindowsRemoteControl/Security/RefreshTokenManager.cs create mode 100644 WindowsRemoteControl/Security/TokenValues.cs create mode 100644 WindowsRemoteControl/Security/ValidationRecord.cs create mode 100644 WindowsRemoteControl/Security/WindowsAuthController.cs delete mode 100644 WindowsRemoteControl/WindowsExecutionCommands.cs diff --git a/CortexRemote.csproj b/CortexRemote.csproj index e13128a..c6aab74 100644 --- a/CortexRemote.csproj +++ b/CortexRemote.csproj @@ -5,6 +5,7 @@ net9.0-windows enable enable + true true true 9e3a541b-7d5a-407c-b1e6-96c24aa9c872 @@ -16,9 +17,9 @@ - + diff --git a/Extensions/ApiEndpointExtensionMethod/RemoteEndpointExtension.cs b/Extensions/ApiEndpointExtensionMethod/RemoteEndpointExtension.cs deleted file mode 100644 index 850af03..0000000 --- a/Extensions/ApiEndpointExtensionMethod/RemoteEndpointExtension.cs +++ /dev/null @@ -1,39 +0,0 @@ -using CortexRemote.Interfaces; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace CortexRemote.Extensions.ApiEndpointExtensionMethod; - -public static class RemoteEndpointExtension -{ - public static IEndpointConventionBuilder MapRemote(this IEndpointRouteBuilder endpoints) - { - ArgumentNullException.ThrowIfNull(endpoints); - - var executionCommands = endpoints.ServiceProvider.GetRequiredService(); - - var routeGroup = endpoints.MapGroup("/command"); - - routeGroup.MapPost("/shutdown", async context => - { - await executionCommands.ShutdownCommand(); - context.Response.StatusCode = StatusCodes.Status200OK; - }); - - routeGroup.MapPost("/restart", async context => - { - await executionCommands.RestartCommand(); - context.Response.StatusCode = StatusCodes.Status200OK; - }); - - routeGroup.MapPost("/lock", async context => - { - await executionCommands.LockCommand(); - context.Response.StatusCode = StatusCodes.Status200OK; - }); - - return routeGroup; - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index ecbb3ce..f9da0c4 100644 --- a/Program.cs +++ b/Program.cs @@ -1,12 +1,11 @@ -using CortexRemote.Extensions.ApiEndpointExtensionMethod; +using System.Threading.RateLimiting; using CortexRemote.Interfaces; using CortexRemote.WindowsRemoteControl; using CortexRemote.WindowsRemoteControl.Security; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; using Serilog; @@ -15,52 +14,61 @@ builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(context.Configuration)); -builder.Services.AddAuthentication(options => -{ - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; -}).AddJwtBearer(options => -{ - options.TokenValidationParameters = new TokenValidationParameters - { - ValidateIssuer = true, - ValidateAudience = true, - ValidateLifetime = true, - ValidateIssuerSigningKey = true, - ValidIssuer = GetWindowsTokenParameters.ApplicationName, - ValidAudience = "localhost", - IssuerSigningKey = new RsaSecurityKey(GetWindowsTokenParameters.CreateOrGetRSA()) - }; -}); - builder.WebHost.ConfigureKestrel(config => { config.ListenAnyIP(5000); }); +builder.Services.AddAuthentication() + .AddJwtBearer(options => + { + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = GetWindowsTokenParameters.ApplicationName, + ValidAudience = "user", + ClockSkew = TimeSpan.Zero, + IssuerSigningKey = new RsaSecurityKey( + GetWindowsTokenParameters.CreateOrGetRSA()) + }; + }); + builder.Services.AddAuthorization(); +builder.Services.AddRateLimiter(rateLimiterOptions => +{ + rateLimiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + + rateLimiterOptions.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => + RateLimitPartition.GetFixedWindowLimiter( + partitionKey: httpContext.User.Identity?.Name ?? + httpContext.Request.Headers.Host.ToString(), + factory: _ => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 36, + QueueLimit = 0, + Window = TimeSpan.FromMinutes(1) + })); +}); + builder.Services.AddHostedService(); -builder.Services.AddHostedService(); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +var app = builder.Build(); -builder.Services.AddSingleton(); +app.UseAuthentication(); +app.UseAuthorization(); -var app = builder.Build(); +app.UseRateLimiter(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger() - .UseSwaggerUI(); -} +app.UseHttpsRedirection(); -app.UseAuthentication() - .UseAuthorization() - .UseHttpsRedirection(); +app.MapAuthentication(); -app.MapRemote(); - //.RequireAuthorization(); +app.MapRemote() + .RequireAuthorization(); await app.RunAsync(); \ No newline at end of file diff --git a/WindowsRemoteControl/RemoteEndpointExtension.cs b/WindowsRemoteControl/RemoteEndpointExtension.cs new file mode 100644 index 0000000..c7c7cc8 --- /dev/null +++ b/WindowsRemoteControl/RemoteEndpointExtension.cs @@ -0,0 +1,54 @@ +using System.Runtime.InteropServices; +using CortexRemote.Interfaces; +using CortexRemote.WindowsRemoteControl.WindowsApi.Enums; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +namespace CortexRemote.WindowsRemoteControl; + +public static partial class RemoteEndpointExtension +{ + [LibraryImport("user32.dll", SetLastError = true)] + private static partial void LockWorkStation(); + + [LibraryImport("user32.dll", SetLastError = true)] + private static partial void ExitWindowsEx( + ShutdownTypes uExitCode, + uint dwReason = SHTDN_REASON_FLAG_PLANNED); + + // https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes + private const uint SHTDN_REASON_FLAG_PLANNED = 0x80000000; + + public static IEndpointConventionBuilder MapRemote(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var routeGroup = endpoints.MapGroup("/command"); + + routeGroup.MapPost("/shutdown", _ => + { + try + { + ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN | ShutdownTypes.EWX_SHUTDOWN); + } + catch (Exception exception) + { + + } + }); + + routeGroup.MapPost("/restart", context => + { + ExitWindowsEx(ShutdownTypes.EWX_REBOOT); + }); + + routeGroup.MapPost("/lock", context => + { + LockWorkStation(); + }); + + return routeGroup; + } +} \ No newline at end of file diff --git a/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs b/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs index 2469e57..82ae243 100644 --- a/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs +++ b/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs @@ -30,19 +30,17 @@ public static RSA CreateOrGetRSA() return rsa; } - public static byte[] CreateOrGetRefreshToken(out bool firstRun) + public static byte[] CreateOrGetRefreshToken() { var encryptedFile = GetFilePathFromAppData(REFRESH_TOKEN_FILE); if (File.Exists(encryptedFile)) { - firstRun = false; return WindowsDataProtection.Decrypt(encryptedFile); } var bytes = CreateRefreshToken(); WindowsDataProtection.Encrypt(encryptedFile, bytes); - firstRun = true; return bytes; } diff --git a/WindowsRemoteControl/Security/RefreshTokenManager.cs b/WindowsRemoteControl/Security/RefreshTokenManager.cs deleted file mode 100644 index 586049b..0000000 --- a/WindowsRemoteControl/Security/RefreshTokenManager.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Microsoft.Extensions.Hosting; -using QRCoder; - -namespace CortexRemote.WindowsRemoteControl.Security; - -public class RefreshTokenManager : IHostedService -{ - public Task StartAsync(CancellationToken cancellationToken) - { - var refreshToken = Convert.ToBase64String( - GetWindowsTokenParameters.CreateOrGetRefreshToken(out var firstRun)); - - try - { - if (!firstRun) - return Task.CompletedTask; - - using var qrGenerator = new QRCodeGenerator(); - using var qrCodeData = qrGenerator.CreateQrCode(refreshToken, QRCodeGenerator.ECCLevel.Q); - using var qrCode = new QRCode(qrCodeData); - var qrCodeBitmap = qrCode.GetGraphic(10); - - var uiThread = new Thread(() => - { - var form = new Form() - { - Width = qrCodeBitmap.Width + 20, - Height = qrCodeBitmap.Height + 20, - FormBorderStyle = FormBorderStyle.FixedDialog, - StartPosition = FormStartPosition.CenterScreen, - Text = "Your Refresh Token. Do not share this token with anyone!!!" - }; - - var pictureBox = new PictureBox() - { - Dock = DockStyle.Fill, - Image = qrCodeBitmap, - SizeMode = PictureBoxSizeMode.StretchImage - }; - - form.Controls.Add(pictureBox); - - Application.Run(form); - }); - - uiThread.SetApartmentState(ApartmentState.STA); - uiThread.Start(); - } - finally - { - refreshToken = null; // Remove ref so the string can be garbage-collected. - } - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/WindowsRemoteControl/Security/TokenValues.cs b/WindowsRemoteControl/Security/TokenValues.cs new file mode 100644 index 0000000..134f29a --- /dev/null +++ b/WindowsRemoteControl/Security/TokenValues.cs @@ -0,0 +1,3 @@ +namespace CortexRemote.WindowsRemoteControl.Security; + +public record TokenValues(string Token, string Type, DateTime Expiration); \ No newline at end of file diff --git a/WindowsRemoteControl/Security/ValidationRecord.cs b/WindowsRemoteControl/Security/ValidationRecord.cs new file mode 100644 index 0000000..e9d4f21 --- /dev/null +++ b/WindowsRemoteControl/Security/ValidationRecord.cs @@ -0,0 +1,3 @@ +namespace CortexRemote.WindowsRemoteControl.Security; + +internal record ValidationRecord(string RefreshToken); \ No newline at end of file diff --git a/WindowsRemoteControl/Security/WindowsAuthController.cs b/WindowsRemoteControl/Security/WindowsAuthController.cs new file mode 100644 index 0000000..2cf1a08 --- /dev/null +++ b/WindowsRemoteControl/Security/WindowsAuthController.cs @@ -0,0 +1,47 @@ +using System.IdentityModel.Tokens.Jwt; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.IdentityModel.Tokens; + +namespace CortexRemote.WindowsRemoteControl.Security; + +public static class WindowsAuthController +{ + public static void MapAuthentication(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var routeGroup = endpoints.MapGroup("/auth"); + + routeGroup.MapPost("/refresh", (ValidationRecord validation) => + { + var storedToken = Convert.ToBase64String( + GetWindowsTokenParameters.CreateOrGetRefreshToken()); + + if (!storedToken.Equals(validation.RefreshToken)) + return Results.Unauthorized(); + + var key = new RsaSecurityKey( + GetWindowsTokenParameters.CreateOrGetRSA()); + var credentials = new SigningCredentials( + key: key, + algorithm: SecurityAlgorithms.RsaSha512); + + var token = new JwtSecurityToken( + issuer: GetWindowsTokenParameters.ApplicationName, + audience: "user", + expires: DateTime.UtcNow.AddSeconds(10), + signingCredentials: credentials); + + var JwtTokenBody= new JwtSecurityTokenHandler() + .WriteToken(token); + + return Results.Ok( + new TokenValues( + Token: JwtTokenBody, + Type: "Bearer", + Expiration: token.ValidTo.Date.ToLocalTime())); + }); + } +} \ No newline at end of file diff --git a/WindowsRemoteControl/Security/WindowsDataProtection.cs b/WindowsRemoteControl/Security/WindowsDataProtection.cs index 3671b94..afd4918 100644 --- a/WindowsRemoteControl/Security/WindowsDataProtection.cs +++ b/WindowsRemoteControl/Security/WindowsDataProtection.cs @@ -39,18 +39,20 @@ private static string GetWMISpecificToken(string WMIClass, string propertyName) .Select(o => o[propertyName]?.ToString()) .FirstOrDefault(); - if (hardwareInstances is null) - throw new Exception($"Property '{propertyName}' not found in WMI class {WMIClass}"); //Todo: Create exception handling middleware - - return hardwareInstances; + return hardwareInstances ?? + throw new Exception( + $"Property '{propertyName}' not found in WMI class {WMIClass}"); } private static byte[] GenerateHardwareHash() { - var processorId = GetWMISpecificToken("Win32_Processor", "ProcessorId") ?? - throw new Exception("GetWMISpecificToken: Failed to get ProcessorId"); // Todo: Create exception handling middleware - var motherboardId = GetWMISpecificToken("Win32_BaseBoard", "SerialNumber") ?? - throw new Exception("GetWMISpecificToken: Failed to get motherboard ID"); // Todo: Create exception handling middleware + var processorId = GetWMISpecificToken( + WMIClass: "Win32_Processor", + propertyName: "ProcessorId"); + + var motherboardId = GetWMISpecificToken( + WMIClass: "Win32_BaseBoard", + propertyName: "SerialNumber"); var hashBytes = Encoding.UTF8.GetBytes($"{processorId}{motherboardId}"); diff --git a/WindowsRemoteControl/WindowsExecutionCommands.cs b/WindowsRemoteControl/WindowsExecutionCommands.cs deleted file mode 100644 index 0989d7c..0000000 --- a/WindowsRemoteControl/WindowsExecutionCommands.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Runtime.InteropServices; -using CortexRemote.Interfaces; -using CortexRemote.WindowsRemoteControl.WindowsApi.Enums; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace CortexRemote.WindowsRemoteControl; - -public partial class WindowsExecutionCommands : IExecutionCommands -{ - [LibraryImport("user32.dll", SetLastError = true)] - private static partial void LockWorkStation(); - - [LibraryImport("user32.dll", SetLastError = true)] - private static partial void ExitWindowsEx( - ShutdownTypes uExitCode, - uint dwReason = SHTDN_REASON_FLAG_PLANNED); - - // https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes - private const uint SHTDN_REASON_FLAG_PLANNED = 0x80000000; - - private readonly ILogger _logger; - - public WindowsExecutionCommands( - ILogger logger) - { - _logger = logger; - } - - public Task ShutdownCommand() - { - ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN | ShutdownTypes.EWX_SHUTDOWN); - return Task.FromResult(Results.Ok()); - } - - public Task LockCommand() - { - LockWorkStation(); - return Task.FromResult(Results.Ok()); - } - - public Task RestartCommand() - { - ExitWindowsEx(ShutdownTypes.EWX_REBOOT); - return Task.FromResult(Results.Ok()); - } -} \ No newline at end of file From b0eb0f59a4a6407e2c6a3b92696b5dd18eef0750 Mon Sep 17 00:00:00 2001 From: Kuro <96239346+Kuarma@users.noreply.github.com> Date: Mon, 29 Sep 2025 00:31:25 +0200 Subject: [PATCH 2/5] Scalar added to Proj and fixed Endpoints --- CortexRemote.csproj | 9 +-- Endpoints/ErrorResponse.cs | 3 + Endpoints/MapRemoteControlEndpoints.cs | 68 +++++++++++++++++++ Endpoints/MapSecurityEndpoints.cs | 58 ++++++++++++++++ .../TokenRecords/AuthenticationRequest.cs | 9 +++ Endpoints/TokenRecords/TokenResponse.cs | 8 +++ Interfaces/IExecutionCommands.cs | 10 --- Middleware/BearerSecuritySchemeTransformer.cs | 33 +++++++++ Middleware/JsonDateTimeConverter.cs | 15 ++++ Program.cs | 45 +++++++++--- .../Enums/Privileges.cs | 2 +- .../Enums/ShutdownTypes.cs | 2 +- .../ProcessTokenService.cs | 14 ++-- .../Security/GetWindowsTokenParameters.cs | 2 +- .../Security/WindowsDataProtection.cs | 2 +- .../WindowsApi => Windows}/Structs/LUID.cs | 2 +- .../Structs/LuIdAttributes.cs | 2 +- .../Structs/TokenPrivileges.cs | 2 +- .../RemoteEndpointExtension.cs | 54 --------------- WindowsRemoteControl/Security/TokenValues.cs | 3 - .../Security/ValidationRecord.cs | 3 - .../Security/WindowsAuthController.cs | 47 ------------- 22 files changed, 245 insertions(+), 148 deletions(-) create mode 100644 Endpoints/ErrorResponse.cs create mode 100644 Endpoints/MapRemoteControlEndpoints.cs create mode 100644 Endpoints/MapSecurityEndpoints.cs create mode 100644 Endpoints/TokenRecords/AuthenticationRequest.cs create mode 100644 Endpoints/TokenRecords/TokenResponse.cs delete mode 100644 Interfaces/IExecutionCommands.cs create mode 100644 Middleware/BearerSecuritySchemeTransformer.cs create mode 100644 Middleware/JsonDateTimeConverter.cs rename {WindowsRemoteControl/WindowsApi => Windows}/Enums/Privileges.cs (60%) rename {WindowsRemoteControl/WindowsApi => Windows}/Enums/ShutdownTypes.cs (93%) rename WindowsRemoteControl/WindowsPrivilegeManager.cs => Windows/ProcessTokenService.cs (90%) rename {WindowsRemoteControl => Windows}/Security/GetWindowsTokenParameters.cs (97%) rename {WindowsRemoteControl => Windows}/Security/WindowsDataProtection.cs (97%) rename {WindowsRemoteControl/WindowsApi => Windows}/Structs/LUID.cs (70%) rename {WindowsRemoteControl/WindowsApi => Windows}/Structs/LuIdAttributes.cs (71%) rename {WindowsRemoteControl/WindowsApi => Windows}/Structs/TokenPrivileges.cs (74%) delete mode 100644 WindowsRemoteControl/RemoteEndpointExtension.cs delete mode 100644 WindowsRemoteControl/Security/TokenValues.cs delete mode 100644 WindowsRemoteControl/Security/ValidationRecord.cs delete mode 100644 WindowsRemoteControl/Security/WindowsAuthController.cs diff --git a/CortexRemote.csproj b/CortexRemote.csproj index c6aab74..714a4cd 100644 --- a/CortexRemote.csproj +++ b/CortexRemote.csproj @@ -8,14 +8,16 @@ true true true - 9e3a541b-7d5a-407c-b1e6-96c24aa9c872 + + + @@ -29,9 +31,4 @@ - - - - - diff --git a/Endpoints/ErrorResponse.cs b/Endpoints/ErrorResponse.cs new file mode 100644 index 0000000..0745803 --- /dev/null +++ b/Endpoints/ErrorResponse.cs @@ -0,0 +1,3 @@ +namespace CortexRemote.Endpoints; + +public abstract record ErrorResponse(int StatusCode, string Message); \ No newline at end of file diff --git a/Endpoints/MapRemoteControlEndpoints.cs b/Endpoints/MapRemoteControlEndpoints.cs new file mode 100644 index 0000000..bd8c627 --- /dev/null +++ b/Endpoints/MapRemoteControlEndpoints.cs @@ -0,0 +1,68 @@ +using System.Runtime.InteropServices; +using CortexRemote.Windows.Enums; +using CortexRemote.Windows.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace CortexRemote.Endpoints; + +public static partial class MapRemoteControlEndpoints +{ + [LibraryImport("user32.dll", SetLastError = true)] + private static partial void LockWorkStation(); + + [LibraryImport("user32.dll", SetLastError = true)] + private static partial void ExitWindowsEx( + ShutdownTypes uExitCode, + uint dwReason = SHTDN_REASON_FLAG_PLANNED); + + // https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes + private const uint SHTDN_REASON_FLAG_PLANNED = 0x80000000; + + public static IEndpointConventionBuilder MapCommands(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var routeGroup = endpoints.MapGroup("/command"); + + routeGroup.MapPost("/shutdown", () => + { + ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN | ShutdownTypes.EWX_SHUTDOWN); + return Results.Ok(); + }) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .WithName("Shutdown System") + .WithTags("Commands") + .WithSummary("Turn your computer off"); + + routeGroup.MapPost("/restart", () => + { + ExitWindowsEx(ShutdownTypes.EWX_REBOOT); + return Results.Ok(); + }) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .WithName("Restarts System") + .WithTags("Commands") + .WithSummary("Restart your computer"); + + + routeGroup.MapPost("/lock", () => + { + LockWorkStation(); + return Results.Ok(); + }) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status403Forbidden) + .WithName("Lock System") + .WithTags("Commands") + .WithSummary("Lock your computer"); + + return routeGroup; + } +} \ No newline at end of file diff --git a/Endpoints/MapSecurityEndpoints.cs b/Endpoints/MapSecurityEndpoints.cs new file mode 100644 index 0000000..4b57988 --- /dev/null +++ b/Endpoints/MapSecurityEndpoints.cs @@ -0,0 +1,58 @@ +using System.IdentityModel.Tokens.Jwt; +using CortexRemote.Endpoints.TokenRecords; +using CortexRemote.Windows.Security; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Routing; +using Microsoft.IdentityModel.Tokens; + +namespace CortexRemote.Endpoints; + +public static class MapSecurityEndpoints +{ + public static void MapAuthentication(this IEndpointRouteBuilder endpoints) + { + ArgumentNullException.ThrowIfNull(endpoints); + + var routeGroup = endpoints.MapGroup("/auth"); + + routeGroup.MapPost("/refresh", + Results, UnauthorizedHttpResult> (AuthenticationRequest authenticationRequest) => + { + var storedToken = Convert.ToBase64String( + GetWindowsTokenParameters.CreateOrGetRefreshToken()); + + if (!storedToken.Equals(authenticationRequest.RefreshToken)) + return TypedResults.Unauthorized(); + + var key = new RsaSecurityKey( + GetWindowsTokenParameters.CreateOrGetRSA()); + var credentials = new SigningCredentials( + key: key, + algorithm: SecurityAlgorithms.RsaSha512); + + var token = new JwtSecurityToken( + issuer: GetWindowsTokenParameters.ApplicationName, + audience: "user", + expires: DateTime.UtcNow.AddSeconds(30), + signingCredentials: credentials); + + var JwtTokenBody = new JwtSecurityTokenHandler() + .WriteToken(token); + + return TypedResults.Ok( + new TokenResponse + { + AccessToken = JwtTokenBody, + Expiration = token.ValidTo.ToLocalTime(), + Type = "Bearer" + }); + }) + .Produces(StatusCodes.Status200OK) + .Produces(StatusCodes.Status401Unauthorized) + .WithName("Authorization") + .WithTags("Validation") + .WithSummary("Request new access token"); + } +} \ No newline at end of file diff --git a/Endpoints/TokenRecords/AuthenticationRequest.cs b/Endpoints/TokenRecords/AuthenticationRequest.cs new file mode 100644 index 0000000..5de0ca4 --- /dev/null +++ b/Endpoints/TokenRecords/AuthenticationRequest.cs @@ -0,0 +1,9 @@ +using JetBrains.Annotations; + +namespace CortexRemote.Endpoints.TokenRecords; + +[UsedImplicitly] +public record AuthenticationRequest +{ + public required string RefreshToken { get; init; } +}; \ No newline at end of file diff --git a/Endpoints/TokenRecords/TokenResponse.cs b/Endpoints/TokenRecords/TokenResponse.cs new file mode 100644 index 0000000..45143b4 --- /dev/null +++ b/Endpoints/TokenRecords/TokenResponse.cs @@ -0,0 +1,8 @@ +namespace CortexRemote.Endpoints.TokenRecords; + +public record TokenResponse +{ + public string AccessToken { get; set; } = null!; + public string Type { get; set; } = null!; + public DateTime Expiration { get; set; } +} diff --git a/Interfaces/IExecutionCommands.cs b/Interfaces/IExecutionCommands.cs deleted file mode 100644 index 3f840c4..0000000 --- a/Interfaces/IExecutionCommands.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace CortexRemote.Interfaces; - -public interface IExecutionCommands -{ - public Task ShutdownCommand(); - public Task LockCommand(); - public Task RestartCommand(); -} \ No newline at end of file diff --git a/Middleware/BearerSecuritySchemeTransformer.cs b/Middleware/BearerSecuritySchemeTransformer.cs new file mode 100644 index 0000000..7efc48c --- /dev/null +++ b/Middleware/BearerSecuritySchemeTransformer.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.OpenApi; +using Microsoft.OpenApi.Models; + +namespace CortexRemote.Middleware; + +internal sealed class BearerSecuritySchemeTransformer( + IAuthenticationSchemeProvider authenticationSchemeProvider + ) : IOpenApiDocumentTransformer +{ + public async Task TransformAsync( + OpenApiDocument document, + OpenApiDocumentTransformerContext context, + CancellationToken cancellationToken) + { + var authenticationSchemes = await authenticationSchemeProvider.GetAllSchemesAsync(); + if (authenticationSchemes.Any(authScheme => authScheme.Name == "Bearer")) + { + var requirements = new Dictionary + { + ["Bearer"] = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.Http, + Scheme = "Bearer", + In = ParameterLocation.Header, + BearerFormat = "Json Web Token" + } + }; + document.Components ??= new OpenApiComponents(); + document.Components.SecuritySchemes = requirements; + } + } +} \ No newline at end of file diff --git a/Middleware/JsonDateTimeConverter.cs b/Middleware/JsonDateTimeConverter.cs new file mode 100644 index 0000000..47f88a3 --- /dev/null +++ b/Middleware/JsonDateTimeConverter.cs @@ -0,0 +1,15 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CortexRemote.Middleware; + +public class JsonDateTimeConverter : JsonConverter +{ + private const string TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => DateTime.Parse(reader.GetString() ?? ""); + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToLocalTime().ToString(TIME_FORMAT)); +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index f9da0c4..d7fabab 100644 --- a/Program.cs +++ b/Program.cs @@ -1,15 +1,19 @@ using System.Threading.RateLimiting; -using CortexRemote.Interfaces; -using CortexRemote.WindowsRemoteControl; -using CortexRemote.WindowsRemoteControl.Security; +using CortexRemote.Endpoints; +using CortexRemote.Middleware; +using CortexRemote.Windows; +using CortexRemote.Windows.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Json; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; +using Scalar.AspNetCore; using Serilog; -var builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(); builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(context.Configuration)); @@ -19,6 +23,16 @@ config.ListenAnyIP(5000); }); +builder.Services.Configure(options => +{ + options.SerializerOptions.Converters.Add(new JsonDateTimeConverter()); +}); + +builder.Services.AddOpenApi(options => +{ + options.AddDocumentTransformer(); +}); + builder.Services.AddAuthentication() .AddJwtBearer(options => { @@ -55,20 +69,29 @@ })); }); -builder.Services.AddHostedService(); +builder.Services.AddHostedService(); var app = builder.Build(); -app.UseAuthentication(); -app.UseAuthorization(); - -app.UseRateLimiter(); +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); + app.MapScalarApiReference(options => + { + options + .WithTitle("Cortex Remote") + .WithTheme(ScalarTheme.DeepSpace) + .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); + }); +} app.UseHttpsRedirection(); +app.MapCommands() + .RequireAuthorization(); + app.MapAuthentication(); -app.MapRemote() - .RequireAuthorization(); +app.UseRateLimiter(); await app.RunAsync(); \ No newline at end of file diff --git a/WindowsRemoteControl/WindowsApi/Enums/Privileges.cs b/Windows/Enums/Privileges.cs similarity index 60% rename from WindowsRemoteControl/WindowsApi/Enums/Privileges.cs rename to Windows/Enums/Privileges.cs index 9599b53..08d3e93 100644 --- a/WindowsRemoteControl/WindowsApi/Enums/Privileges.cs +++ b/Windows/Enums/Privileges.cs @@ -1,4 +1,4 @@ -namespace CortexRemote.WindowsRemoteControl.WindowsApi.Enums; +namespace CortexRemote.Windows.Enums; [Flags] public enum Privileges : uint diff --git a/WindowsRemoteControl/WindowsApi/Enums/ShutdownTypes.cs b/Windows/Enums/ShutdownTypes.cs similarity index 93% rename from WindowsRemoteControl/WindowsApi/Enums/ShutdownTypes.cs rename to Windows/Enums/ShutdownTypes.cs index 0f13a0f..a846f6c 100644 --- a/WindowsRemoteControl/WindowsApi/Enums/ShutdownTypes.cs +++ b/Windows/Enums/ShutdownTypes.cs @@ -1,4 +1,4 @@ -namespace CortexRemote.WindowsRemoteControl.WindowsApi.Enums; +namespace CortexRemote.Windows.Enums; // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-exitwindowsex diff --git a/WindowsRemoteControl/WindowsPrivilegeManager.cs b/Windows/ProcessTokenService.cs similarity index 90% rename from WindowsRemoteControl/WindowsPrivilegeManager.cs rename to Windows/ProcessTokenService.cs index 19b84ae..b50f6b8 100644 --- a/WindowsRemoteControl/WindowsPrivilegeManager.cs +++ b/Windows/ProcessTokenService.cs @@ -1,14 +1,14 @@ using System.ComponentModel; using System.Diagnostics; using System.Runtime.InteropServices; -using CortexRemote.WindowsRemoteControl.WindowsApi.Enums; -using CortexRemote.WindowsRemoteControl.WindowsApi.Structs; +using CortexRemote.Windows.Enums; +using CortexRemote.Windows.Structs; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace CortexRemote.WindowsRemoteControl; +namespace CortexRemote.Windows; -internal partial class WindowsPrivilegeManager : IHostedService +internal partial class ProcessTokenService : IHostedService { [LibraryImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] @@ -43,10 +43,10 @@ private static partial bool LookupPrivilegeValueW( private const int SE_PRIVILEGES_COUNT = 1; private IntPtr _tokenHandle; - private readonly ILogger _logger; + private readonly ILogger _logger; - public WindowsPrivilegeManager( - ILogger logger) + public ProcessTokenService( + ILogger logger) { _logger = logger; } diff --git a/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs b/Windows/Security/GetWindowsTokenParameters.cs similarity index 97% rename from WindowsRemoteControl/Security/GetWindowsTokenParameters.cs rename to Windows/Security/GetWindowsTokenParameters.cs index 82ae243..d05ed31 100644 --- a/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs +++ b/Windows/Security/GetWindowsTokenParameters.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.Security.Cryptography; -namespace CortexRemote.WindowsRemoteControl.Security; +namespace CortexRemote.Windows.Security; public static class GetWindowsTokenParameters { diff --git a/WindowsRemoteControl/Security/WindowsDataProtection.cs b/Windows/Security/WindowsDataProtection.cs similarity index 97% rename from WindowsRemoteControl/Security/WindowsDataProtection.cs rename to Windows/Security/WindowsDataProtection.cs index afd4918..c5e5e46 100644 --- a/WindowsRemoteControl/Security/WindowsDataProtection.cs +++ b/Windows/Security/WindowsDataProtection.cs @@ -2,7 +2,7 @@ using System.Security.Cryptography; using System.Text; -namespace CortexRemote.WindowsRemoteControl.Security; +namespace CortexRemote.Windows.Security; public static class WindowsDataProtection { diff --git a/WindowsRemoteControl/WindowsApi/Structs/LUID.cs b/Windows/Structs/LUID.cs similarity index 70% rename from WindowsRemoteControl/WindowsApi/Structs/LUID.cs rename to Windows/Structs/LUID.cs index 674a04d..6082e73 100644 --- a/WindowsRemoteControl/WindowsApi/Structs/LUID.cs +++ b/Windows/Structs/LUID.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace CortexRemote.WindowsRemoteControl.WindowsApi.Structs; +namespace CortexRemote.Windows.Structs; [StructLayout(LayoutKind.Sequential)] public struct LUID diff --git a/WindowsRemoteControl/WindowsApi/Structs/LuIdAttributes.cs b/Windows/Structs/LuIdAttributes.cs similarity index 71% rename from WindowsRemoteControl/WindowsApi/Structs/LuIdAttributes.cs rename to Windows/Structs/LuIdAttributes.cs index 44433ae..747e2b3 100644 --- a/WindowsRemoteControl/WindowsApi/Structs/LuIdAttributes.cs +++ b/Windows/Structs/LuIdAttributes.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace CortexRemote.WindowsRemoteControl.WindowsApi.Structs; +namespace CortexRemote.Windows.Structs; [StructLayout(LayoutKind.Sequential)] public struct LuIdAttributes diff --git a/WindowsRemoteControl/WindowsApi/Structs/TokenPrivileges.cs b/Windows/Structs/TokenPrivileges.cs similarity index 74% rename from WindowsRemoteControl/WindowsApi/Structs/TokenPrivileges.cs rename to Windows/Structs/TokenPrivileges.cs index b74a478..e3b67d3 100644 --- a/WindowsRemoteControl/WindowsApi/Structs/TokenPrivileges.cs +++ b/Windows/Structs/TokenPrivileges.cs @@ -1,6 +1,6 @@ using System.Runtime.InteropServices; -namespace CortexRemote.WindowsRemoteControl.WindowsApi.Structs; +namespace CortexRemote.Windows.Structs; [StructLayout(LayoutKind.Sequential)] public struct TokenPrivileges diff --git a/WindowsRemoteControl/RemoteEndpointExtension.cs b/WindowsRemoteControl/RemoteEndpointExtension.cs deleted file mode 100644 index c7c7cc8..0000000 --- a/WindowsRemoteControl/RemoteEndpointExtension.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Runtime.InteropServices; -using CortexRemote.Interfaces; -using CortexRemote.WindowsRemoteControl.WindowsApi.Enums; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; - -namespace CortexRemote.WindowsRemoteControl; - -public static partial class RemoteEndpointExtension -{ - [LibraryImport("user32.dll", SetLastError = true)] - private static partial void LockWorkStation(); - - [LibraryImport("user32.dll", SetLastError = true)] - private static partial void ExitWindowsEx( - ShutdownTypes uExitCode, - uint dwReason = SHTDN_REASON_FLAG_PLANNED); - - // https://learn.microsoft.com/en-us/windows/win32/shutdown/system-shutdown-reason-codes - private const uint SHTDN_REASON_FLAG_PLANNED = 0x80000000; - - public static IEndpointConventionBuilder MapRemote(this IEndpointRouteBuilder endpoints) - { - ArgumentNullException.ThrowIfNull(endpoints); - - var routeGroup = endpoints.MapGroup("/command"); - - routeGroup.MapPost("/shutdown", _ => - { - try - { - ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN | ShutdownTypes.EWX_SHUTDOWN); - } - catch (Exception exception) - { - - } - }); - - routeGroup.MapPost("/restart", context => - { - ExitWindowsEx(ShutdownTypes.EWX_REBOOT); - }); - - routeGroup.MapPost("/lock", context => - { - LockWorkStation(); - }); - - return routeGroup; - } -} \ No newline at end of file diff --git a/WindowsRemoteControl/Security/TokenValues.cs b/WindowsRemoteControl/Security/TokenValues.cs deleted file mode 100644 index 134f29a..0000000 --- a/WindowsRemoteControl/Security/TokenValues.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace CortexRemote.WindowsRemoteControl.Security; - -public record TokenValues(string Token, string Type, DateTime Expiration); \ No newline at end of file diff --git a/WindowsRemoteControl/Security/ValidationRecord.cs b/WindowsRemoteControl/Security/ValidationRecord.cs deleted file mode 100644 index e9d4f21..0000000 --- a/WindowsRemoteControl/Security/ValidationRecord.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace CortexRemote.WindowsRemoteControl.Security; - -internal record ValidationRecord(string RefreshToken); \ No newline at end of file diff --git a/WindowsRemoteControl/Security/WindowsAuthController.cs b/WindowsRemoteControl/Security/WindowsAuthController.cs deleted file mode 100644 index 2cf1a08..0000000 --- a/WindowsRemoteControl/Security/WindowsAuthController.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.IdentityModel.Tokens.Jwt; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.IdentityModel.Tokens; - -namespace CortexRemote.WindowsRemoteControl.Security; - -public static class WindowsAuthController -{ - public static void MapAuthentication(this IEndpointRouteBuilder endpoints) - { - ArgumentNullException.ThrowIfNull(endpoints); - - var routeGroup = endpoints.MapGroup("/auth"); - - routeGroup.MapPost("/refresh", (ValidationRecord validation) => - { - var storedToken = Convert.ToBase64String( - GetWindowsTokenParameters.CreateOrGetRefreshToken()); - - if (!storedToken.Equals(validation.RefreshToken)) - return Results.Unauthorized(); - - var key = new RsaSecurityKey( - GetWindowsTokenParameters.CreateOrGetRSA()); - var credentials = new SigningCredentials( - key: key, - algorithm: SecurityAlgorithms.RsaSha512); - - var token = new JwtSecurityToken( - issuer: GetWindowsTokenParameters.ApplicationName, - audience: "user", - expires: DateTime.UtcNow.AddSeconds(10), - signingCredentials: credentials); - - var JwtTokenBody= new JwtSecurityTokenHandler() - .WriteToken(token); - - return Results.Ok( - new TokenValues( - Token: JwtTokenBody, - Type: "Bearer", - Expiration: token.ValidTo.Date.ToLocalTime())); - }); - } -} \ No newline at end of file From 57fd89b2310cb438bc1046680035f2093ed733c1 Mon Sep 17 00:00:00 2001 From: Kuro Date: Wed, 1 Oct 2025 17:28:22 +0200 Subject: [PATCH 3/5] Configuration added and refactoring of exception handling --- Endpoints/MapRemoteControlEndpoints.cs | 60 +++++++++++++++---- Endpoints/MapSecurityEndpoints.cs | 2 +- .../AuthenticationRequest.cs | 6 +- .../TokenResponse.cs | 4 +- Middleware/StartupConfiguration.cs | 7 +++ Program.cs | 7 ++- appsettings.json | 53 +++++++++------- 7 files changed, 100 insertions(+), 39 deletions(-) rename Endpoints/{TokenRecords => TokenModel}/AuthenticationRequest.cs (56%) rename Endpoints/{TokenRecords => TokenModel}/TokenResponse.cs (65%) create mode 100644 Middleware/StartupConfiguration.cs diff --git a/Endpoints/MapRemoteControlEndpoints.cs b/Endpoints/MapRemoteControlEndpoints.cs index bd8c627..c397e3b 100644 --- a/Endpoints/MapRemoteControlEndpoints.cs +++ b/Endpoints/MapRemoteControlEndpoints.cs @@ -1,9 +1,10 @@ using System.Runtime.InteropServices; using CortexRemote.Windows.Enums; -using CortexRemote.Windows.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Routing; +using Serilog; namespace CortexRemote.Endpoints; @@ -26,10 +27,23 @@ public static IEndpointConventionBuilder MapCommands(this IEndpointRouteBuilder var routeGroup = endpoints.MapGroup("/command"); - routeGroup.MapPost("/shutdown", () => - { - ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN | ShutdownTypes.EWX_SHUTDOWN); - return Results.Ok(); + routeGroup.MapPost("/shutdown", Results, InternalServerError>() => + { + Log.Information("Command: System Shutdown Command Received at {time}", + DateTime.Now); + + try + { + ExitWindowsEx(ShutdownTypes.EWX_HYBRID_SHUTDOWN + | ShutdownTypes.EWX_SHUTDOWN); + return TypedResults.Ok( + "System Shutdown Command Received"); + } + catch (Exception exception) + { + Log.Error("Shutdown Error: {exception}", exception); + return TypedResults.InternalServerError(); + } }) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) @@ -38,10 +52,22 @@ public static IEndpointConventionBuilder MapCommands(this IEndpointRouteBuilder .WithTags("Commands") .WithSummary("Turn your computer off"); - routeGroup.MapPost("/restart", () => + routeGroup.MapPost("/restart", Results, InternalServerError> () => { - ExitWindowsEx(ShutdownTypes.EWX_REBOOT); - return Results.Ok(); + Log.Information("Command: System Restart Command Received at {time}", + DateTime.Now); + + try + { + ExitWindowsEx(ShutdownTypes.EWX_REBOOT); + return TypedResults.Ok( + "System Restart Command Received"); + } + catch (Exception exception) + { + Log.Error("Restart Error: {exception}", exception); + return TypedResults.InternalServerError(); + } }) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) @@ -51,10 +77,22 @@ public static IEndpointConventionBuilder MapCommands(this IEndpointRouteBuilder .WithSummary("Restart your computer"); - routeGroup.MapPost("/lock", () => + routeGroup.MapPost("/lock", Results, InternalServerError> () => { - LockWorkStation(); - return Results.Ok(); + Log.Information("Command: System Lock Command Received at {time}", + DateTime.Now); + + try + { + LockWorkStation(); + return TypedResults.Ok( + "System Lock Command Received"); + } + catch (Exception exception) + { + Log.Error("Lock Error: {exception}", exception); + return TypedResults.InternalServerError(); + } }) .Produces(StatusCodes.Status200OK) .Produces(StatusCodes.Status401Unauthorized) diff --git a/Endpoints/MapSecurityEndpoints.cs b/Endpoints/MapSecurityEndpoints.cs index 4b57988..8cc1579 100644 --- a/Endpoints/MapSecurityEndpoints.cs +++ b/Endpoints/MapSecurityEndpoints.cs @@ -1,5 +1,5 @@ using System.IdentityModel.Tokens.Jwt; -using CortexRemote.Endpoints.TokenRecords; +using CortexRemote.Endpoints.TokenModel; using CortexRemote.Windows.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; diff --git a/Endpoints/TokenRecords/AuthenticationRequest.cs b/Endpoints/TokenModel/AuthenticationRequest.cs similarity index 56% rename from Endpoints/TokenRecords/AuthenticationRequest.cs rename to Endpoints/TokenModel/AuthenticationRequest.cs index 5de0ca4..ad0ecb1 100644 --- a/Endpoints/TokenRecords/AuthenticationRequest.cs +++ b/Endpoints/TokenModel/AuthenticationRequest.cs @@ -1,9 +1,9 @@ using JetBrains.Annotations; -namespace CortexRemote.Endpoints.TokenRecords; +namespace CortexRemote.Endpoints.TokenModel; [UsedImplicitly] -public record AuthenticationRequest +public class AuthenticationRequest { public required string RefreshToken { get; init; } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/Endpoints/TokenRecords/TokenResponse.cs b/Endpoints/TokenModel/TokenResponse.cs similarity index 65% rename from Endpoints/TokenRecords/TokenResponse.cs rename to Endpoints/TokenModel/TokenResponse.cs index 45143b4..c8a0119 100644 --- a/Endpoints/TokenRecords/TokenResponse.cs +++ b/Endpoints/TokenModel/TokenResponse.cs @@ -1,6 +1,6 @@ -namespace CortexRemote.Endpoints.TokenRecords; +namespace CortexRemote.Endpoints.TokenModel; -public record TokenResponse +public class TokenResponse { public string AccessToken { get; set; } = null!; public string Type { get; set; } = null!; diff --git a/Middleware/StartupConfiguration.cs b/Middleware/StartupConfiguration.cs new file mode 100644 index 0000000..2800720 --- /dev/null +++ b/Middleware/StartupConfiguration.cs @@ -0,0 +1,7 @@ +namespace CortexRemote.Middleware; + +internal sealed class StartupConfiguration +{ + public required int Port { get; init; } + public required bool RemoteAccessEnabled { get; init; } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index d7fabab..d5ca41b 100644 --- a/Program.cs +++ b/Program.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.IdentityModel.Tokens; @@ -18,9 +19,13 @@ builder.Host.UseSerilog((context, loggerConfiguration) => loggerConfiguration.ReadFrom.Configuration(context.Configuration)); +var configuration = builder.Services.BuildServiceProvider().GetRequiredService(); +// builder.Services.AddOptions().BindConfiguration("Configuration") +// .ValidateDataAnnotations(); + builder.WebHost.ConfigureKestrel(config => { - config.ListenAnyIP(5000); + config.ListenAnyIP(configuration.GetValue("Configuration:Port")); }); builder.Services.Configure(options => diff --git a/appsettings.json b/appsettings.json index 4337f88..b0c260c 100644 --- a/appsettings.json +++ b/appsettings.json @@ -1,23 +1,34 @@ { - "Serilog": { - "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File"], - "MinimumLevel": { - "Default": "Information" - }, - "WriteTo": [ - { - "Name": "Console" - }, - { - "Name": "File", - "Args": { - "path": "logs/log-.json", - "rollingInterval": "Day", - "rollOnFileSizeLimit": true, - "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog" - } - } - ], - "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] - } + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File" + ], + "MinimumLevel": { + "Default": "Information" + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "logs/log-.json", + "rollingInterval": "Day", + "rollOnFileSizeLimit": true, + "formatter": "Serilog.Formatting.Json.JsonFormatter, Serilog" + } + } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithThreadId" + ] + }, + "Configuration": { + "Port": 4000, + "RemoteAccessEnabled": false + } } \ No newline at end of file From 0106b6f9a2a4cd5ffda00673952d54913bbb83e9 Mon Sep 17 00:00:00 2001 From: Kuro Date: Sat, 4 Oct 2025 12:24:49 +0200 Subject: [PATCH 4/5] Refactor --- Endpoints/MapSecurityEndpoints.cs | 14 ++- Middleware/StartupConfiguration.cs | 32 +++++- Program.cs | 16 +-- Windows/DirectoryManager.cs | 70 ++++++++++++ Windows/Security/GetWindowsTokenParameters.cs | 66 ----------- ...ataProtection.cs => HardwareIdentifier.cs} | 47 ++------ Windows/Security/KeyHandler.cs | 103 ++++++++++++++++++ Windows/{ => Security}/ProcessTokenService.cs | 2 +- 8 files changed, 231 insertions(+), 119 deletions(-) create mode 100644 Windows/DirectoryManager.cs delete mode 100644 Windows/Security/GetWindowsTokenParameters.cs rename Windows/Security/{WindowsDataProtection.cs => HardwareIdentifier.cs} (56%) create mode 100644 Windows/Security/KeyHandler.cs rename Windows/{ => Security}/ProcessTokenService.cs (98%) diff --git a/Endpoints/MapSecurityEndpoints.cs b/Endpoints/MapSecurityEndpoints.cs index 8cc1579..79d4e96 100644 --- a/Endpoints/MapSecurityEndpoints.cs +++ b/Endpoints/MapSecurityEndpoints.cs @@ -1,5 +1,6 @@ using System.IdentityModel.Tokens.Jwt; using CortexRemote.Endpoints.TokenModel; +using CortexRemote.Windows; using CortexRemote.Windows.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -11,30 +12,31 @@ namespace CortexRemote.Endpoints; public static class MapSecurityEndpoints { - public static void MapAuthentication(this IEndpointRouteBuilder endpoints) + public static void MapAuthentication(this IEndpointRouteBuilder endpoints, IKeyHandler keyHandler) { ArgumentNullException.ThrowIfNull(endpoints); - + var routeGroup = endpoints.MapGroup("/auth"); routeGroup.MapPost("/refresh", Results, UnauthorizedHttpResult> (AuthenticationRequest authenticationRequest) => { var storedToken = Convert.ToBase64String( - GetWindowsTokenParameters.CreateOrGetRefreshToken()); + keyHandler.ImportRefreshToken()); if (!storedToken.Equals(authenticationRequest.RefreshToken)) return TypedResults.Unauthorized(); var key = new RsaSecurityKey( - GetWindowsTokenParameters.CreateOrGetRSA()); + keyHandler.ImportAsymmetricKey(KeyHandler.SigningKeyFileName)); var credentials = new SigningCredentials( key: key, algorithm: SecurityAlgorithms.RsaSha512); var token = new JwtSecurityToken( - issuer: GetWindowsTokenParameters.ApplicationName, - audience: "user", + issuer: DirectoryManager.APPLICATION_NAME_FOLDER, + audience: DirectoryManager.APPLICATION_NAME_FOLDER + + Environment.MachineName, expires: DateTime.UtcNow.AddSeconds(30), signingCredentials: credentials); diff --git a/Middleware/StartupConfiguration.cs b/Middleware/StartupConfiguration.cs index 2800720..4a24e39 100644 --- a/Middleware/StartupConfiguration.cs +++ b/Middleware/StartupConfiguration.cs @@ -1,7 +1,31 @@ -namespace CortexRemote.Middleware; +using CortexRemote.Windows.Security; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; -internal sealed class StartupConfiguration +namespace CortexRemote.Middleware; + +internal sealed class StartupConfiguration : IHostedService { - public required int Port { get; init; } - public required bool RemoteAccessEnabled { get; init; } + private readonly ILogger _logger; + private readonly IKeyHandler _keyHandler; + + public StartupConfiguration( + ILogger logger, + IKeyHandler keyHandler + ) + { + _logger = logger; + _keyHandler = keyHandler; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } \ No newline at end of file diff --git a/Program.cs b/Program.cs index d5ca41b..84c7b26 100644 --- a/Program.cs +++ b/Program.cs @@ -20,8 +20,9 @@ loggerConfiguration.ReadFrom.Configuration(context.Configuration)); var configuration = builder.Services.BuildServiceProvider().GetRequiredService(); -// builder.Services.AddOptions().BindConfiguration("Configuration") -// .ValidateDataAnnotations(); + +builder.Services.AddSingleton(); +var rsa = builder.Services.BuildServiceProvider().GetRequiredService(); builder.WebHost.ConfigureKestrel(config => { @@ -37,7 +38,6 @@ { options.AddDocumentTransformer(); }); - builder.Services.AddAuthentication() .AddJwtBearer(options => { @@ -47,11 +47,12 @@ ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, - ValidIssuer = GetWindowsTokenParameters.ApplicationName, - ValidAudience = "user", + ValidIssuer = DirectoryManager.APPLICATION_NAME_FOLDER, + ValidAudience = DirectoryManager.APPLICATION_NAME_FOLDER + + Environment.MachineName, ClockSkew = TimeSpan.Zero, IssuerSigningKey = new RsaSecurityKey( - GetWindowsTokenParameters.CreateOrGetRSA()) + rsa.ImportAsymmetricKey(KeyHandler.SigningKeyFileName)) }; }); @@ -75,6 +76,7 @@ }); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); var app = builder.Build(); @@ -95,7 +97,7 @@ app.MapCommands() .RequireAuthorization(); -app.MapAuthentication(); +app.MapAuthentication(rsa); app.UseRateLimiter(); diff --git a/Windows/DirectoryManager.cs b/Windows/DirectoryManager.cs new file mode 100644 index 0000000..7d276e7 --- /dev/null +++ b/Windows/DirectoryManager.cs @@ -0,0 +1,70 @@ +namespace CortexRemote.Windows; + +internal static class DirectoryManager +{ + public const string APPLICATION_NAME_FOLDER = "CortexRemote"; + public const string KEY_FOLDER = "Keys"; + + public static bool TryGetFolder(string filename, string folderName, + Environment.SpecialFolder specialFolder, + out string? folderPath) + { + if (string.IsNullOrWhiteSpace(folderName) || + string.IsNullOrWhiteSpace(filename) || + !TryGetEnvironmentPath(specialFolder, + out var specialFolderPath)) + { + folderPath = null; + return false; + } + + folderPath = Path.Combine( + path1: specialFolderPath!, + path2: APPLICATION_NAME_FOLDER, + path3: folderName, + path4: filename); + + return true; + } + + public static bool TryGetOrCreateFolder(string filename, string folderName, + Environment.SpecialFolder specialFolder, + out string? folderPath) + { + if (string.IsNullOrWhiteSpace(folderName) || + string.IsNullOrWhiteSpace(filename) || + !TryGetEnvironmentPath(specialFolder, + out var specialFolderPath)) + { + folderPath = null; + return false; + } + + folderPath = Path.Combine( + path1: specialFolderPath!, + path2: APPLICATION_NAME_FOLDER, + path3: folderName); + + if (!Directory.Exists(folderPath)) + Directory.CreateDirectory(folderPath); + + folderPath = Path.Combine( + folderPath, filename); + + return true; + } + + private static bool TryGetEnvironmentPath(Environment.SpecialFolder specialFolder, + out string? folderPath) + { + var specialFolderPath = Environment.GetFolderPath(specialFolder); + if (!Directory.Exists(specialFolderPath)) + { + folderPath = null; + return false; + } + + folderPath = specialFolderPath; + return true; + } +} \ No newline at end of file diff --git a/Windows/Security/GetWindowsTokenParameters.cs b/Windows/Security/GetWindowsTokenParameters.cs deleted file mode 100644 index d05ed31..0000000 --- a/Windows/Security/GetWindowsTokenParameters.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Reflection; -using System.Security.Cryptography; - -namespace CortexRemote.Windows.Security; - -public static class GetWindowsTokenParameters -{ - private const string PRIVATE_KEY_FILE = "privateKey.bin"; - private const string REFRESH_TOKEN_FILE = "refreshToken.bin"; - - public static readonly string ApplicationName = Assembly.GetExecutingAssembly().GetName().Name!; - - public static RSA CreateOrGetRSA() - { - var encryptedFile = GetFilePathFromAppData(PRIVATE_KEY_FILE); - - var rsa = RSA.Create(2048); - - if (File.Exists(encryptedFile)) - { - var decryptedPrivateKey = WindowsDataProtection.Decrypt(encryptedFile); - rsa.ImportRSAPrivateKey(decryptedPrivateKey, out _); - } - else - { - var privateKey = rsa.ExportRSAPrivateKey(); - WindowsDataProtection.Encrypt(encryptedFile, privateKey); - } - - return rsa; - } - - public static byte[] CreateOrGetRefreshToken() - { - var encryptedFile = GetFilePathFromAppData(REFRESH_TOKEN_FILE); - - if (File.Exists(encryptedFile)) - { - return WindowsDataProtection.Decrypt(encryptedFile); - } - - var bytes = CreateRefreshToken(); - WindowsDataProtection.Encrypt(encryptedFile, bytes); - return bytes; - } - - private static string GetFilePathFromAppData(string fileName) - { - var path = Path.Combine( - path1: Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - path2: Assembly.GetExecutingAssembly().GetName().Name!, - path3: fileName); - - var directory = Path.GetDirectoryName(path); - - if (!Directory.Exists(directory)) - Directory.CreateDirectory(directory!); - - return path; - } - - private static byte[] CreateRefreshToken() - { - return RandomNumberGenerator.GetBytes(64); - } -} \ No newline at end of file diff --git a/Windows/Security/WindowsDataProtection.cs b/Windows/Security/HardwareIdentifier.cs similarity index 56% rename from Windows/Security/WindowsDataProtection.cs rename to Windows/Security/HardwareIdentifier.cs index c5e5e46..e43d903 100644 --- a/Windows/Security/WindowsDataProtection.cs +++ b/Windows/Security/HardwareIdentifier.cs @@ -4,31 +4,23 @@ namespace CortexRemote.Windows.Security; -public static class WindowsDataProtection +internal static class HardwareIdentifier { - public static byte[] Decrypt(string encryptedFilePath) + public static byte[] GenerateHardwareHash() { - var encryptedFile = File.ReadAllBytes(encryptedFilePath); - var decryptedData = ProtectedData.Unprotect( - encryptedData: encryptedFile, - optionalEntropy: GenerateHardwareHash(), - scope: DataProtectionScope.CurrentUser); + var processorId = GetWMISpecificToken( + WMIClass: "Win32_Processor", + propertyName: "ProcessorId"); + + var motherboardId = GetWMISpecificToken( + WMIClass: "Win32_BaseBoard", + propertyName: "SerialNumber"); + + var hashBytes = Encoding.UTF8.GetBytes($"{processorId}{motherboardId}"); - return decryptedData; + return SHA256.HashData(hashBytes); } - public static void Encrypt(string encryptedFileLocation, byte[] value) - { - var encryptedKey = ProtectedData.Protect( - userData: value, - optionalEntropy: GenerateHardwareHash(), - scope: DataProtectionScope.CurrentUser); - - File.WriteAllBytes( - path: encryptedFileLocation, - bytes: encryptedKey); - } - private static string GetWMISpecificToken(string WMIClass, string propertyName) { using var packageSpecificToken = new ManagementClass(WMIClass); @@ -43,19 +35,4 @@ private static string GetWMISpecificToken(string WMIClass, string propertyName) throw new Exception( $"Property '{propertyName}' not found in WMI class {WMIClass}"); } - - private static byte[] GenerateHardwareHash() - { - var processorId = GetWMISpecificToken( - WMIClass: "Win32_Processor", - propertyName: "ProcessorId"); - - var motherboardId = GetWMISpecificToken( - WMIClass: "Win32_BaseBoard", - propertyName: "SerialNumber"); - - var hashBytes = Encoding.UTF8.GetBytes($"{processorId}{motherboardId}"); - - return SHA256.HashData(hashBytes); - } } \ No newline at end of file diff --git a/Windows/Security/KeyHandler.cs b/Windows/Security/KeyHandler.cs new file mode 100644 index 0000000..4955ba1 --- /dev/null +++ b/Windows/Security/KeyHandler.cs @@ -0,0 +1,103 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Logging; + +namespace CortexRemote.Windows.Security; + +internal sealed class KeyHandler : IKeyHandler +{ + public const string SigningKeyFileName = "CortexRemoteSigningKey.key"; + public const string EncryptionKeyFileName = "CortexRemoteEncryptionKey.key"; + public const string RefreshTokenFileName = "CortexRemoteRefreshToken.key"; + + private readonly ILogger _logger; + + public KeyHandler(ILogger logger) + { + _logger = logger; + } + + public void ExportAsymmetricKey( + string keyFileName) + { + var rsa = RSA.Create(); + if (!DirectoryManager.TryGetOrCreateFolder( + filename: keyFileName, + folderName: DirectoryManager.KEY_FOLDER, + specialFolder: Environment.SpecialFolder.ProgramFiles, + out var path)) + throw new Exception("Failed to create the key folder."); + + Encrypt( + encryptedFileLocation: path!, + key: rsa.ExportRSAPrivateKey()); + } + + public RSA ImportAsymmetricKey( + string keyFileName) + { + var rsa = RSA.Create(); + if (!DirectoryManager.TryGetOrCreateFolder( + filename: keyFileName, + folderName: DirectoryManager.KEY_FOLDER, + specialFolder: Environment.SpecialFolder.ProgramFiles, + out var path)) + throw new Exception("Failed to create the key folder."); + + var privateKey = Decrypt( + encryptedFileLocation: path!); + + rsa.ImportRSAPrivateKey(privateKey, out _); + return rsa; + } + + public void ExportRefreshToken(string keyFileName) + { + if (!DirectoryManager.TryGetOrCreateFolder( + filename: keyFileName, + folderName: DirectoryManager.KEY_FOLDER, + specialFolder: Environment.SpecialFolder.ProgramFiles, + out var path)) + throw new Exception("Failed to create the key folder."); + + Encrypt( + encryptedFileLocation: path!, + key: RandomNumberGenerator.GetBytes(32)); + } + + public byte[] ImportRefreshToken(string keyFileName = RefreshTokenFileName) + { + if (!DirectoryManager.TryGetOrCreateFolder( + filename: keyFileName, + folderName: DirectoryManager.KEY_FOLDER, + specialFolder: Environment.SpecialFolder.ProgramFiles, + out var path)) + throw new Exception("Failed to create the key folder."); + + return Decrypt( + encryptedFileLocation: path!); + } + + private static void Encrypt(string encryptedFileLocation, byte[] key) => + File.WriteAllBytes( + path: encryptedFileLocation, + bytes: ProtectedData.Protect( + userData: key, + optionalEntropy: HardwareIdentifier.GenerateHardwareHash(), + scope: DataProtectionScope.CurrentUser)); + + private static byte[] Decrypt(string encryptedFileLocation) => + ProtectedData.Unprotect( + encryptedData: File.ReadAllBytes(encryptedFileLocation), + optionalEntropy: HardwareIdentifier.GenerateHardwareHash(), + scope: DataProtectionScope.CurrentUser); +} + +public interface IKeyHandler +{ + public void ExportAsymmetricKey(string keyFileName); + public RSA ImportAsymmetricKey(string keyFileName); + + public void ExportRefreshToken(string keyFileName); + public byte[] ImportRefreshToken(string keyFileName = KeyHandler.RefreshTokenFileName); +} \ No newline at end of file diff --git a/Windows/ProcessTokenService.cs b/Windows/Security/ProcessTokenService.cs similarity index 98% rename from Windows/ProcessTokenService.cs rename to Windows/Security/ProcessTokenService.cs index b50f6b8..38b8cce 100644 --- a/Windows/ProcessTokenService.cs +++ b/Windows/Security/ProcessTokenService.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -namespace CortexRemote.Windows; +namespace CortexRemote.Windows.Security; internal partial class ProcessTokenService : IHostedService { From 66cdfe8de42f4c1631967e12014b92f26213397f Mon Sep 17 00:00:00 2001 From: Kuro <96239346+Kuarma@users.noreply.github.com> Date: Sat, 4 Oct 2025 17:17:36 +0200 Subject: [PATCH 5/5] Update README.md --- README.md | 73 +------------------------------------------------------ 1 file changed, 1 insertion(+), 72 deletions(-) diff --git a/README.md b/README.md index ead3d0b..1ec3b76 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,2 @@ # CortexRemote - -**CortexRemote** is a side project designed to remotely control your PC via a Cloudflare tunnel using the Windows API. The application aims to provide a secure, cross-platform solution to control a PC like an IoT device. Cross-platform support is currently a work in progress. - ---- - -## Features -- Remote shutdown, restart, and log-off functionality via CLI -- Cloudflare tunnel integration for secure remote access (can be disabled) -- Minimal configuration using JSON settings -- Partial cross-platform design (Linux support planned) - ---- - -## Requirements -- [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0) -- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/) -- A configured `appsettings.json` file - -## Configuration (Does not work currently) -Create a file named `appsettings.json` in the root directory: - -### Manual Tunnel Configuration (Self-Hosted) -```json -{ - "Cloudflare": { - "TunnelId": "your-tunnel-id", - "AuthToken": "your-auth-token", - "ListeningPort": 4999, - "RemoteAccessEnabled": true - } -} -``` - -### Auto Tunnel (Fast-Tunnel) -```json -{ - "Cloudflare": { - "ListeningPort": 4999, - "RemoteAccessEnabled": true - } -} -``` - -## Usage (Work in Progress) -CortexRemoteControl API is currently under development. - -- Commands are received via Http POST. -- A Swagger UI is available at `http://localhost:/swagger/index.html` when running in Development mode. -- Authentication is not yet finalized. (Just Token verification at the moment) -- The token is currently read from `appsettings.json`, which is **not secure** and will be replaced in future versions. - -### ⚠️⚠️⚠️ DO NOT EXPOSE THIS SERVICE TO THE INTERNET YET ⚠️⚠️⚠️ - -## Installation - -1. Clone the repo: -```bash -git clone https://github.com/Kuarma/CortexRemote.git -cd CortexRemote -``` -2. Restore packages: -```bash -dotnet restore -``` -3. Build project: -```bash -dotnet build -``` -4. Run the project: -```bash -dotnet run -``` +Readme coming soon...