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