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...