From e6bab7e371a524a1512ed991f445ffde69b79b8c Mon Sep 17 00:00:00 2001
From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com>
Date: Tue, 9 Jun 2026 19:26:34 +0200
Subject: [PATCH] Add TcpRpc server and client and split crypto code into
multiple classes
---
BcAesCrypto.cs | 108 --------------------------
Networking/AesPacketCrypto.cs | 16 ++++
Networking/IPacketCrypto.cs | 8 ++
Networking/TcpConnection.cs | 18 +++++
Networking/TcpMessage.cs | 8 ++
Networking/TcpRpcBase.cs | 141 ++++++++++++++++++++++++++++++++++
Networking/TcpRpcClient.cs | 92 ++++++++++++++++++++++
Networking/TcpRpcServer.cs | 94 +++++++++++++++++++++++
Processes/DesktopProcess.cs | 2 +-
Utils/AesGcmCrypto.cs | 69 +++++++++++++++++
Utils/PasswordKeyDeriver.cs | 26 +++++++
11 files changed, 473 insertions(+), 109 deletions(-)
delete mode 100644 BcAesCrypto.cs
create mode 100644 Networking/AesPacketCrypto.cs
create mode 100644 Networking/IPacketCrypto.cs
create mode 100644 Networking/TcpConnection.cs
create mode 100644 Networking/TcpMessage.cs
create mode 100644 Networking/TcpRpcBase.cs
create mode 100644 Networking/TcpRpcClient.cs
create mode 100644 Networking/TcpRpcServer.cs
create mode 100644 Utils/AesGcmCrypto.cs
create mode 100644 Utils/PasswordKeyDeriver.cs
diff --git a/BcAesCrypto.cs b/BcAesCrypto.cs
deleted file mode 100644
index ff35eef..0000000
--- a/BcAesCrypto.cs
+++ /dev/null
@@ -1,108 +0,0 @@
-using Org.BouncyCastle.Crypto;
-using Org.BouncyCastle.Crypto.Digests;
-using Org.BouncyCastle.Crypto.Engines;
-using Org.BouncyCastle.Crypto.Generators;
-using Org.BouncyCastle.Crypto.Modes;
-using Org.BouncyCastle.Crypto.Parameters;
-
-using System.Security.Cryptography;
-using System.Text;
-
-public static class BcAesCrypto
-{
- private const int KeySize = 32; // 256-bit AES
- private const int SaltSize = 16; // 128-bit salt
- private const int NonceSize = 12; // GCM standard
- private const int TagSize = 128; // authentication tag (bits)
- private const int Iterations = 150_000; // PBKDF2 cost factor
-
- // -----------------------------
- // PBKDF2 Key Derivation
- // -----------------------------
- private static byte[] DeriveKey(string password, byte[] salt)
- {
- Pkcs5S2ParametersGenerator generator = new(new Sha256Digest());
-
- generator.Init(
- PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()),
- salt,
- Iterations
- );
-
- KeyParameter keyParam = (KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8);
- return keyParam.GetKey();
- }
-
- // -----------------------------
- // Encrypt
- // -----------------------------
- public static string Encrypt(string plainText, string password)
- {
- byte[] salt = new byte[SaltSize];
- RandomNumberGenerator.Fill(salt);
-
- byte[] key = DeriveKey(password, salt);
-
- byte[] nonce = new byte[NonceSize];
- RandomNumberGenerator.Fill(nonce);
-
- GcmBlockCipher cipher = new(new AesEngine());
-
- cipher.Init(true, new AeadParameters(
- new KeyParameter(key),
- TagSize,
- nonce
- ));
-
- byte[] input = Encoding.UTF8.GetBytes(plainText);
- byte[] output = new byte[cipher.GetOutputSize(input.Length)];
-
- int len = cipher.ProcessBytes(input, 0, input.Length, output, 0);
- _ = cipher.DoFinal(output, len);
-
- // Combine: salt + nonce + ciphertext
- byte[] result = new byte[salt.Length + nonce.Length + output.Length];
-
- Buffer.BlockCopy(salt, 0, result, 0, salt.Length);
- Buffer.BlockCopy(nonce, 0, result, salt.Length, nonce.Length);
- Buffer.BlockCopy(output, 0, result, salt.Length + nonce.Length, output.Length);
-
- return Convert.ToBase64String(result);
- }
-
- // -----------------------------
- // Decrypt
- // -----------------------------
- public static string Decrypt(string cipherText, string password)
- {
- byte[] data = Convert.FromBase64String(cipherText);
-
- byte[] salt = new byte[SaltSize];
- byte[] nonce = new byte[NonceSize];
-
- Buffer.BlockCopy(data, 0, salt, 0, SaltSize);
- Buffer.BlockCopy(data, SaltSize, nonce, 0, NonceSize);
-
- int cipherLength = data.Length - SaltSize - NonceSize;
- byte[] cipherBytes = new byte[cipherLength];
-
- Buffer.BlockCopy(data, SaltSize + NonceSize, cipherBytes, 0, cipherLength);
-
- byte[] key = DeriveKey(password, salt);
-
- GcmBlockCipher cipher = new(new AesEngine());
-
- cipher.Init(false, new AeadParameters(
- new KeyParameter(key),
- TagSize,
- nonce
- ));
-
- byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)];
-
- int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0);
- _ = cipher.DoFinal(plain, len);
-
- return Encoding.UTF8.GetString(plain);
- }
-}
diff --git a/Networking/AesPacketCrypto.cs b/Networking/AesPacketCrypto.cs
new file mode 100644
index 0000000..e24c197
--- /dev/null
+++ b/Networking/AesPacketCrypto.cs
@@ -0,0 +1,16 @@
+using RemSox.Utils;
+
+namespace RemSox.Networking;
+
+internal class AesPacketCrypto(byte[] key) : IPacketCrypto
+{
+ public byte[] Decrypt(byte[] data)
+ {
+ return AesGcmCrypto.Decrypt(data, key);
+ }
+
+ public byte[] Encrypt(byte[] data)
+ {
+ return AesGcmCrypto.Encrypt(data, key);
+ }
+}
diff --git a/Networking/IPacketCrypto.cs b/Networking/IPacketCrypto.cs
new file mode 100644
index 0000000..5d6e0ee
--- /dev/null
+++ b/Networking/IPacketCrypto.cs
@@ -0,0 +1,8 @@
+namespace RemSox.Networking;
+
+public interface IPacketCrypto
+{
+ byte[] Encrypt(byte[] data);
+
+ byte[] Decrypt(byte[] data);
+}
\ No newline at end of file
diff --git a/Networking/TcpConnection.cs b/Networking/TcpConnection.cs
new file mode 100644
index 0000000..8a28cb0
--- /dev/null
+++ b/Networking/TcpConnection.cs
@@ -0,0 +1,18 @@
+using System.Net.Sockets;
+
+///
+/// Simple wrapper around TcpClient to hold connection-scoped resources safely.
+///
+public class TcpConnection(TcpClient client) : IDisposable
+{
+ public TcpClient Client { get; } = client;
+ public NetworkStream Stream { get; } = client.GetStream();
+ public SemaphoreSlim SendLock { get; } = new(1, 1);
+
+ public void Dispose()
+ {
+ Stream.Dispose();
+ Client.Dispose();
+ SendLock.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/Networking/TcpMessage.cs b/Networking/TcpMessage.cs
new file mode 100644
index 0000000..5a7a7d4
--- /dev/null
+++ b/Networking/TcpMessage.cs
@@ -0,0 +1,8 @@
+namespace RemSox.Networking;
+
+public class TcpMessage
+{
+ public string Type { get; set; } = string.Empty;
+ public string RequestId { get; set; } = string.Empty;
+ public byte[] Payload { get; set; } = [];
+}
\ No newline at end of file
diff --git a/Networking/TcpRpcBase.cs b/Networking/TcpRpcBase.cs
new file mode 100644
index 0000000..818578c
--- /dev/null
+++ b/Networking/TcpRpcBase.cs
@@ -0,0 +1,141 @@
+using RemSox.Networking;
+
+using System.Buffers.Binary;
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Sockets;
+using System.Text.Json;
+
+public abstract class TcpRpcBase(IPacketCrypto? crypto = null)
+{
+ protected readonly IPacketCrypto? crypto = crypto;
+ protected readonly Dictionary> handlers = [];
+ protected readonly ConcurrentDictionary> pendingRequests = new();
+
+ // Security Guard: Prevent OOM/DoS via oversized length headers (Default: 32MB)
+ protected const int MaxMessageSize = 32 * 1024 * 1024;
+
+ ///
+ /// Base message registration for handlers tracking the sending connection.
+ ///
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ public void ListenTo(string type, Func handler)
+ {
+ handlers[type] = async (conn, msg) =>
+ {
+ T data = JsonSerializer.Deserialize(msg.Payload)!;
+ await handler(conn, data);
+ };
+ }
+
+ ///
+ /// Overloaded listener where connection mapping can be ignored (Convenient for Clients).
+ ///
+ public void ListenTo(string type, Func handler)
+ {
+ ListenTo(type, async (_, data) => await handler(data));
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ protected async Task SendRaw(TcpConnection conn, TcpMessage msg)
+ {
+ byte[] data = JsonSerializer.SerializeToUtf8Bytes(msg);
+
+ if (crypto is not null)
+ {
+ data = crypto.Encrypt(data);
+ }
+
+ // Allocate a single contiguous frame buffer to prevent inter-thread fragmentation
+ // and eliminate redundant Socket Write Syscalls.
+ byte[] packet = new byte[4 + data.Length];
+ BinaryPrimitives.WriteInt32LittleEndian(packet, data.Length);
+ Array.Copy(data, 0, packet, 4, data.Length);
+
+ // Enforce sequence safety across multiple threads pushing data out of a singular stream
+ await conn.SendLock.WaitAsync();
+ try
+ {
+ await conn.Stream.WriteAsync(packet);
+ }
+ finally
+ {
+ _ = conn.SendLock.Release();
+ }
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ protected async Task HandleConnection(TcpConnection conn, CancellationToken token = default)
+ {
+ NetworkStream stream = conn.Stream;
+ byte[] lengthBytes = new byte[4];
+
+ try
+ {
+ while (!token.IsCancellationRequested && conn.Client.Connected)
+ {
+ // 1. Frame Length Read
+ await stream.ReadExactlyAsync(lengthBytes, token);
+ int length = BinaryPrimitives.ReadInt32LittleEndian(lengthBytes);
+
+ // DoS Payload Protection Check
+ if (length is <= 0 or > MaxMessageSize)
+ {
+ throw new InvalidDataException($"Protocol violation: Received packet length of {length} bytes exceeds limits.");
+ }
+
+ // 2. Body Payload Read
+ byte[] buffer = new byte[length];
+ await stream.ReadExactlyAsync(buffer, token);
+
+ if (crypto is not null)
+ {
+ buffer = crypto.Decrypt(buffer);
+ }
+
+ TcpMessage? msg = JsonSerializer.Deserialize(buffer);
+ if (msg is null)
+ {
+ continue;
+ }
+
+ // 3. Response Handler Check
+ if (pendingRequests.TryGetValue(msg.RequestId, out TaskCompletionSource? tcs))
+ {
+ _ = tcs.TrySetResult(msg.Payload);
+ _ = pendingRequests.TryRemove(msg.RequestId, out _);
+ continue;
+ }
+
+ // 4. Inbound Router Handler
+ if (handlers.TryGetValue(msg.Type, out Func? handler))
+ {
+ // Decouple handler execution to the ThreadPool so slow business logic
+ // doesn't bottleneck packet processing from the network stream interface.
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await handler(conn, msg);
+ }
+ catch
+ {
+ // Operational log placement here for user exceptions inside delegates
+ }
+ }, token);
+ }
+ }
+ }
+ catch
+ {
+ // Explicitly handles natural dropping scenarios cleanly
+ }
+ finally
+ {
+ OnConnectionClosed(conn);
+ conn.Dispose();
+ }
+ }
+
+ protected abstract void OnConnectionClosed(TcpConnection conn);
+}
\ No newline at end of file
diff --git a/Networking/TcpRpcClient.cs b/Networking/TcpRpcClient.cs
new file mode 100644
index 0000000..c2f02de
--- /dev/null
+++ b/Networking/TcpRpcClient.cs
@@ -0,0 +1,92 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Sockets;
+using System.Text.Json;
+
+namespace RemSox.Networking;
+
+public class TcpRpcClient(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
+{
+ private TcpConnection? connection;
+
+ public async Task ConnectAsync(string host, int port)
+ {
+ TcpClient client = new();
+ await client.ConnectAsync(host, port);
+
+ connection = new TcpConnection(client);
+
+ // Run network monitoring task loop background-detached
+ _ = HandleConnection(connection);
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ public async Task RequestAsync(string type, TReq request, TimeSpan timeout = default)
+ {
+ if (connection is null)
+ {
+ throw new InvalidOperationException("Client not connected.");
+ }
+
+ if (timeout == default)
+ {
+ timeout = TimeSpan.FromSeconds(30); // Default fallback timeout
+ }
+
+ string requestId = Guid.NewGuid().ToString();
+ TaskCompletionSource tcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ pendingRequests[requestId] = tcs;
+
+ try
+ {
+ await SendRaw(connection, new TcpMessage
+ {
+ Type = type,
+ RequestId = requestId,
+ Payload = JsonSerializer.SerializeToUtf8Bytes(request)
+ });
+
+ // Enforce async timeout safety to prevent permanent dictionary leaks on dropped calls
+ using CancellationTokenSource timeoutCts = new(timeout);
+ await using (timeoutCts.Token.Register(() => tcs.TrySetCanceled()))
+ {
+ byte[] responseBytes = await tcs.Task;
+ return JsonSerializer.Deserialize(responseBytes)!;
+ }
+ }
+ finally
+ {
+ _ = pendingRequests.TryRemove(requestId, out _);
+ }
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ public Task SendAsync(string type, T data)
+ {
+ if (connection is null)
+ {
+ throw new InvalidOperationException("Client not connected.");
+ }
+
+ return SendRaw(connection, new TcpMessage
+ {
+ Type = type,
+ RequestId = Guid.NewGuid().ToString(),
+ Payload = JsonSerializer.SerializeToUtf8Bytes(data)
+ });
+ }
+
+ protected override void OnConnectionClosed(TcpConnection conn)
+ {
+ if (connection == conn)
+ {
+ connection = null;
+ }
+
+ // Fail-fast all lingering tasks waiting on an dead connection loop
+ foreach (TaskCompletionSource req in pendingRequests.Values)
+ {
+ _ = req.TrySetException(new SocketException((int)SocketError.ConnectionReset));
+ }
+ pendingRequests.Clear();
+ }
+}
\ No newline at end of file
diff --git a/Networking/TcpRpcServer.cs b/Networking/TcpRpcServer.cs
new file mode 100644
index 0000000..ce2542e
--- /dev/null
+++ b/Networking/TcpRpcServer.cs
@@ -0,0 +1,94 @@
+using System.Collections.Concurrent;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Sockets;
+using System.Text.Json;
+
+namespace RemSox.Networking;
+
+public class TcpRpcServer(IPacketCrypto? crypto = null) : TcpRpcBase(crypto)
+{
+ // Fix: Using ConcurrentDictionary to prevent collection errors during structural additions/prunings
+ private readonly ConcurrentDictionary connections = new();
+ private TcpListener? listener;
+ private CancellationTokenSource? cts;
+
+ public async Task StartAsync(int port, CancellationToken token = default)
+ {
+ cts = CancellationTokenSource.CreateLinkedTokenSource(token);
+
+ listener = new TcpListener(IPAddress.Any, port);
+ listener.Start();
+
+ try
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ TcpClient client = await listener.AcceptTcpClientAsync(cts.Token);
+
+ TcpConnection conn = new(client);
+ _ = connections.TryAdd(conn, 0);
+
+ _ = HandleConnection(conn, cts.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected shutdown scenario
+ }
+ }
+
+ public void Stop()
+ {
+ cts?.Cancel();
+ listener?.Stop();
+
+ foreach (TcpConnection c in connections.Keys)
+ {
+ c.Dispose();
+ }
+
+ connections.Clear();
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ public void RespondTo(string type, Func> handler)
+ {
+ handlers[type] = async (conn, msg) =>
+ {
+ TReq req = JsonSerializer.Deserialize(msg.Payload)!;
+ TRes res = await handler(req);
+
+ await SendRaw(conn, new TcpMessage
+ {
+ Type = type,
+ RequestId = msg.RequestId,
+ Payload = JsonSerializer.SerializeToUtf8Bytes(res)
+ });
+ };
+ }
+
+ [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Handled via library constraints")]
+ public Task SendToAll(string type, T data)
+ {
+ byte[] payloadBytes = JsonSerializer.SerializeToUtf8Bytes(data);
+ List sendTasks = [];
+
+ foreach (TcpConnection conn in connections.Keys)
+ {
+ sendTasks.Add(SendRaw(conn, new TcpMessage
+ {
+ Type = type,
+ RequestId = Guid.NewGuid().ToString(),
+ Payload = payloadBytes
+ }));
+ }
+
+ return Task.WhenAll(sendTasks);
+ }
+
+ protected override void OnConnectionClosed(TcpConnection conn)
+ {
+ _ = connections.TryRemove(conn, out _);
+ }
+}
\ No newline at end of file
diff --git a/Processes/DesktopProcess.cs b/Processes/DesktopProcess.cs
index ab91c87..1936abb 100644
--- a/Processes/DesktopProcess.cs
+++ b/Processes/DesktopProcess.cs
@@ -68,7 +68,7 @@ public class DesktopProcess : Process
WindowManager.Update();
// Sleep slightly to yield CPU to the main CLI thread (approx 60 FPS)
- //Thread.Sleep(16);
+ Thread.Sleep(16);
}
IsRunning = false;
diff --git a/Utils/AesGcmCrypto.cs b/Utils/AesGcmCrypto.cs
new file mode 100644
index 0000000..9ca7875
--- /dev/null
+++ b/Utils/AesGcmCrypto.cs
@@ -0,0 +1,69 @@
+using Org.BouncyCastle.Crypto.Engines;
+using Org.BouncyCastle.Crypto.Modes;
+using Org.BouncyCastle.Crypto.Parameters;
+
+using System.Security.Cryptography;
+using System.Text;
+
+namespace RemSox.Utils;
+
+public static class AesGcmCrypto
+{
+ private const int NonceSize = 12;
+ private const int TagSize = 128;
+
+ public static byte[] Encrypt(byte[] plainData, byte[] key)
+ {
+ byte[] nonce = new byte[NonceSize];
+ RandomNumberGenerator.Fill(nonce);
+
+ GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
+ cipher.Init(true, new AeadParameters(new KeyParameter(key), TagSize, nonce));
+
+ byte[] output = new byte[cipher.GetOutputSize(plainData.Length)];
+
+ int len = cipher.ProcessBytes(plainData, 0, plainData.Length, output, 0);
+ _ = cipher.DoFinal(output, len);
+
+ byte[] result = new byte[nonce.Length + output.Length];
+
+ Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
+ Buffer.BlockCopy(output, 0, result, nonce.Length, output.Length);
+
+ return result;
+ }
+
+ public static string Encrypt(string text, byte[] key)
+ {
+ byte[] data = Encoding.UTF8.GetBytes(text);
+ return Convert.ToBase64String(Encrypt(data, key));
+ }
+
+ public static byte[] Decrypt(byte[] encryptedData, byte[] key)
+ {
+ byte[] nonce = new byte[NonceSize];
+ Buffer.BlockCopy(encryptedData, 0, nonce, 0, NonceSize);
+
+ int cipherLength = encryptedData.Length - NonceSize;
+ byte[] cipherBytes = new byte[cipherLength];
+
+ Buffer.BlockCopy(encryptedData, NonceSize, cipherBytes, 0, cipherLength);
+
+ GcmBlockCipher cipher = new GcmBlockCipher(new AesEngine());
+ cipher.Init(false, new AeadParameters(new KeyParameter(key), TagSize, nonce));
+
+ byte[] plain = new byte[cipher.GetOutputSize(cipherBytes.Length)];
+
+ int len = cipher.ProcessBytes(cipherBytes, 0, cipherBytes.Length, plain, 0);
+ _ = cipher.DoFinal(plain, len);
+
+ return plain;
+ }
+
+ public static string Decrypt(string cipherText, byte[] key)
+ {
+ byte[] data = Convert.FromBase64String(cipherText);
+ byte[] plain = Decrypt(data, key);
+ return Encoding.UTF8.GetString(plain);
+ }
+}
\ No newline at end of file
diff --git a/Utils/PasswordKeyDeriver.cs b/Utils/PasswordKeyDeriver.cs
new file mode 100644
index 0000000..cfc0506
--- /dev/null
+++ b/Utils/PasswordKeyDeriver.cs
@@ -0,0 +1,26 @@
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Digests;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+
+namespace RemSox.Utils;
+
+public static class PasswordKeyDeriver
+{
+ private const int KeySize = 32; // 256-bit key
+ private const int Iterations = 150_000; // PBKDF2 cost factor
+
+ public static byte[] DeriveKey(string password, byte[] salt)
+ {
+ Pkcs5S2ParametersGenerator generator = new Pkcs5S2ParametersGenerator(new Sha256Digest());
+
+ generator.Init(
+ PbeParametersGenerator.Pkcs5PasswordToBytes(password.ToCharArray()),
+ salt,
+ Iterations
+ );
+
+ return ((KeyParameter)generator.GenerateDerivedMacParameters(KeySize * 8))
+ .GetKey();
+ }
+}
\ No newline at end of file