diff --git a/CortexRemote.csproj b/CortexRemote.csproj
index e13128a..714a4cd 100644
--- a/CortexRemote.csproj
+++ b/CortexRemote.csproj
@@ -5,20 +5,23 @@
net9.0-windows
enable
enable
+ true
true
true
- 9e3a541b-7d5a-407c-b1e6-96c24aa9c872
+
+
+
-
+
@@ -28,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..c397e3b
--- /dev/null
+++ b/Endpoints/MapRemoteControlEndpoints.cs
@@ -0,0 +1,106 @@
+using System.Runtime.InteropServices;
+using CortexRemote.Windows.Enums;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.HttpResults;
+using Microsoft.AspNetCore.Routing;
+using Serilog;
+
+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", 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)
+ .Produces(StatusCodes.Status403Forbidden)
+ .WithName("Shutdown System")
+ .WithTags("Commands")
+ .WithSummary("Turn your computer off");
+
+ routeGroup.MapPost("/restart", Results, InternalServerError> () =>
+ {
+ 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)
+ .Produces(StatusCodes.Status403Forbidden)
+ .WithName("Restarts System")
+ .WithTags("Commands")
+ .WithSummary("Restart your computer");
+
+
+ routeGroup.MapPost("/lock", Results, InternalServerError> () =>
+ {
+ 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)
+ .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..79d4e96
--- /dev/null
+++ b/Endpoints/MapSecurityEndpoints.cs
@@ -0,0 +1,60 @@
+using System.IdentityModel.Tokens.Jwt;
+using CortexRemote.Endpoints.TokenModel;
+using CortexRemote.Windows;
+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, IKeyHandler keyHandler)
+ {
+ ArgumentNullException.ThrowIfNull(endpoints);
+
+ var routeGroup = endpoints.MapGroup("/auth");
+
+ routeGroup.MapPost("/refresh",
+ Results, UnauthorizedHttpResult> (AuthenticationRequest authenticationRequest) =>
+ {
+ var storedToken = Convert.ToBase64String(
+ keyHandler.ImportRefreshToken());
+
+ if (!storedToken.Equals(authenticationRequest.RefreshToken))
+ return TypedResults.Unauthorized();
+
+ var key = new RsaSecurityKey(
+ keyHandler.ImportAsymmetricKey(KeyHandler.SigningKeyFileName));
+ var credentials = new SigningCredentials(
+ key: key,
+ algorithm: SecurityAlgorithms.RsaSha512);
+
+ var token = new JwtSecurityToken(
+ issuer: DirectoryManager.APPLICATION_NAME_FOLDER,
+ audience: DirectoryManager.APPLICATION_NAME_FOLDER +
+ Environment.MachineName,
+ 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/TokenModel/AuthenticationRequest.cs b/Endpoints/TokenModel/AuthenticationRequest.cs
new file mode 100644
index 0000000..ad0ecb1
--- /dev/null
+++ b/Endpoints/TokenModel/AuthenticationRequest.cs
@@ -0,0 +1,9 @@
+using JetBrains.Annotations;
+
+namespace CortexRemote.Endpoints.TokenModel;
+
+[UsedImplicitly]
+public class AuthenticationRequest
+{
+ public required string RefreshToken { get; init; }
+}
\ No newline at end of file
diff --git a/Endpoints/TokenModel/TokenResponse.cs b/Endpoints/TokenModel/TokenResponse.cs
new file mode 100644
index 0000000..c8a0119
--- /dev/null
+++ b/Endpoints/TokenModel/TokenResponse.cs
@@ -0,0 +1,8 @@
+namespace CortexRemote.Endpoints.TokenModel;
+
+public class TokenResponse
+{
+ public string AccessToken { get; set; } = null!;
+ public string Type { get; set; } = null!;
+ public DateTime Expiration { get; set; }
+}
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/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/Middleware/StartupConfiguration.cs b/Middleware/StartupConfiguration.cs
new file mode 100644
index 0000000..4a24e39
--- /dev/null
+++ b/Middleware/StartupConfiguration.cs
@@ -0,0 +1,31 @@
+using CortexRemote.Windows.Security;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace CortexRemote.Middleware;
+
+internal sealed class StartupConfiguration : IHostedService
+{
+ 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 ecbb3ce..84c7b26 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,66 +1,104 @@
-using CortexRemote.Extensions.ApiEndpointExtensionMethod;
-using CortexRemote.Interfaces;
-using CortexRemote.WindowsRemoteControl;
-using CortexRemote.WindowsRemoteControl.Security;
-using Microsoft.AspNetCore.Authentication.JwtBearer;
+using System.Threading.RateLimiting;
+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.Configuration;
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));
-builder.Services.AddAuthentication(options =>
+var configuration = builder.Services.BuildServiceProvider().GetRequiredService();
+
+builder.Services.AddSingleton();
+var rsa = builder.Services.BuildServiceProvider().GetRequiredService();
+
+builder.WebHost.ConfigureKestrel(config =>
{
- options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
-}).AddJwtBearer(options =>
+ config.ListenAnyIP(configuration.GetValue("Configuration:Port"));
+});
+
+builder.Services.Configure(options =>
{
- options.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuer = true,
- ValidateAudience = true,
- ValidateLifetime = true,
- ValidateIssuerSigningKey = true,
- ValidIssuer = GetWindowsTokenParameters.ApplicationName,
- ValidAudience = "localhost",
- IssuerSigningKey = new RsaSecurityKey(GetWindowsTokenParameters.CreateOrGetRSA())
- };
+ options.SerializerOptions.Converters.Add(new JsonDateTimeConverter());
});
-builder.WebHost.ConfigureKestrel(config =>
+builder.Services.AddOpenApi(options =>
{
- config.ListenAnyIP(5000);
+ options.AddDocumentTransformer();
});
+builder.Services.AddAuthentication()
+ .AddJwtBearer(options =>
+ {
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = DirectoryManager.APPLICATION_NAME_FOLDER,
+ ValidAudience = DirectoryManager.APPLICATION_NAME_FOLDER +
+ Environment.MachineName,
+ ClockSkew = TimeSpan.Zero,
+ IssuerSigningKey = new RsaSecurityKey(
+ rsa.ImportAsymmetricKey(KeyHandler.SigningKeyFileName))
+ };
+ });
builder.Services.AddAuthorization();
-builder.Services.AddHostedService();
-builder.Services.AddHostedService();
-
-builder.Services.AddEndpointsApiExplorer();
-builder.Services.AddSwaggerGen();
+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.AddSingleton();
+builder.Services.AddHostedService();
+builder.Services.AddHostedService();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
- app.UseSwagger()
- .UseSwaggerUI();
+ app.MapOpenApi();
+ app.MapScalarApiReference(options =>
+ {
+ options
+ .WithTitle("Cortex Remote")
+ .WithTheme(ScalarTheme.DeepSpace)
+ .WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
+ });
}
-app.UseAuthentication()
- .UseAuthorization()
- .UseHttpsRedirection();
+app.UseHttpsRedirection();
+
+app.MapCommands()
+ .RequireAuthorization();
+
+app.MapAuthentication(rsa);
-app.MapRemote();
- //.RequireAuthorization();
+app.UseRateLimiter();
await app.RunAsync();
\ No newline at end of file
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...
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/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/Windows/Security/HardwareIdentifier.cs b/Windows/Security/HardwareIdentifier.cs
new file mode 100644
index 0000000..e43d903
--- /dev/null
+++ b/Windows/Security/HardwareIdentifier.cs
@@ -0,0 +1,38 @@
+using System.Management;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace CortexRemote.Windows.Security;
+
+internal static class HardwareIdentifier
+{
+ public 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);
+ }
+
+ private static string GetWMISpecificToken(string WMIClass, string propertyName)
+ {
+ using var packageSpecificToken = new ManagementClass(WMIClass);
+ using var instances = packageSpecificToken.GetInstances();
+
+ var hardwareInstances = instances
+ .OfType()
+ .Select(o => o[propertyName]?.ToString())
+ .FirstOrDefault();
+
+ return hardwareInstances ??
+ throw new Exception(
+ $"Property '{propertyName}' not found in WMI class {WMIClass}");
+ }
+}
\ 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/WindowsRemoteControl/WindowsPrivilegeManager.cs b/Windows/Security/ProcessTokenService.cs
similarity index 90%
rename from WindowsRemoteControl/WindowsPrivilegeManager.cs
rename to Windows/Security/ProcessTokenService.cs
index 19b84ae..38b8cce 100644
--- a/WindowsRemoteControl/WindowsPrivilegeManager.cs
+++ b/Windows/Security/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.Security;
-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/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/Security/GetWindowsTokenParameters.cs b/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs
deleted file mode 100644
index 2469e57..0000000
--- a/WindowsRemoteControl/Security/GetWindowsTokenParameters.cs
+++ /dev/null
@@ -1,68 +0,0 @@
-using System.Reflection;
-using System.Security.Cryptography;
-
-namespace CortexRemote.WindowsRemoteControl.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(out bool firstRun)
- {
- 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;
- }
-
- 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/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/WindowsDataProtection.cs b/WindowsRemoteControl/Security/WindowsDataProtection.cs
deleted file mode 100644
index 3671b94..0000000
--- a/WindowsRemoteControl/Security/WindowsDataProtection.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using System.Management;
-using System.Security.Cryptography;
-using System.Text;
-
-namespace CortexRemote.WindowsRemoteControl.Security;
-
-public static class WindowsDataProtection
-{
- public static byte[] Decrypt(string encryptedFilePath)
- {
- var encryptedFile = File.ReadAllBytes(encryptedFilePath);
- var decryptedData = ProtectedData.Unprotect(
- encryptedData: encryptedFile,
- optionalEntropy: GenerateHardwareHash(),
- scope: DataProtectionScope.CurrentUser);
-
- return decryptedData;
- }
-
- 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);
- using var instances = packageSpecificToken.GetInstances();
-
- var hardwareInstances = instances
- .OfType()
- .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;
- }
-
- 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 hashBytes = Encoding.UTF8.GetBytes($"{processorId}{motherboardId}");
-
- return SHA256.HashData(hashBytes);
- }
-}
\ No newline at end of file
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
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